diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/AppOpenApiService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/AppOpenApiService.java index 044661f5ba2..bb9bdf7eff0 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/AppOpenApiService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/AppOpenApiService.java @@ -16,10 +16,11 @@ */ package com.ctrip.framework.apollo.openapi.server.service; -import com.ctrip.framework.apollo.openapi.model.MultiResponseEntity; import com.ctrip.framework.apollo.openapi.model.OpenAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenCreateAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterDTO; +import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterInfo; +import com.ctrip.framework.apollo.openapi.model.OpenMissEnvDTO; import org.springframework.lang.NonNull; import java.util.List; @@ -27,9 +28,9 @@ public interface AppOpenApiService { - void createApp(@NonNull OpenCreateAppDTO req); + OpenAppDTO createApp(@NonNull OpenCreateAppDTO req); - List getEnvClusterInfo(String appId); + List getEnvClusters(String appId); List getAllApps(); @@ -39,13 +40,13 @@ public interface AppOpenApiService { void updateApp(OpenAppDTO openAppDTO); - List getAppsBySelf(Set appIds, Integer page, Integer size); + List getAppsWithPageAndSize(Set appIds, Integer page, Integer size); - void createAppInEnv(String env, OpenAppDTO app, String operator); + void createAppInEnv(String env, OpenAppDTO app); OpenAppDTO deleteApp(String appId); - MultiResponseEntity findMissEnvs(String appId); + List findMissEnvs(String appId); - MultiResponseEntity getAppNavTree(String appId); + List getEnvClusterInfos(String appId); } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/InstanceOpenApiService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/InstanceOpenApiService.java new file mode 100644 index 00000000000..b040e8dfd18 --- /dev/null +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/InstanceOpenApiService.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025 Apollo Authors + * + * 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.ctrip.framework.apollo.openapi.server.service; + +import com.ctrip.framework.apollo.openapi.model.OpenInstanceDTO; +import com.ctrip.framework.apollo.openapi.model.OpenInstancePageDTO; + +import java.util.List; +import java.util.Set; + +public interface InstanceOpenApiService { + + int getInstanceCountByNamespace(String appId, String env, String clusterName, + String namespaceName); + + OpenInstancePageDTO getByRelease(String env, long releaseId, int page, int size); + + List getByReleasesNotIn(String env, String appId, String clusterName, + String namespaceName, Set releaseIds); + + OpenInstancePageDTO getByNamespace(String env, String appId, String clusterName, + String namespaceName, String instanceAppId, Integer page, Integer size); +} diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerAppOpenApiService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerAppOpenApiService.java index 50a3ec6de7d..d477c1ecc9c 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerAppOpenApiService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerAppOpenApiService.java @@ -24,11 +24,11 @@ import com.ctrip.framework.apollo.openapi.model.OpenAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenCreateAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterDTO; -import com.ctrip.framework.apollo.openapi.model.MultiResponseEntity; import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterInfo; -import com.ctrip.framework.apollo.openapi.model.RichResponseEntity; +import com.ctrip.framework.apollo.openapi.model.OpenMissEnvDTO; import com.ctrip.framework.apollo.openapi.util.OpenApiModelConverters; import com.ctrip.framework.apollo.portal.component.PortalSettings; +import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.entity.model.AppModel; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.listener.AppDeletionEvent; @@ -83,17 +83,17 @@ private App convert(OpenAppDTO dto) { * @see com.ctrip.framework.apollo.portal.controller.AppController#create(AppModel) */ @Override - public void createApp(OpenCreateAppDTO req) { + public OpenAppDTO createApp(OpenCreateAppDTO req) { App app = convert(req.getApp()); - List admins = req.getAdmins(); + Set admins = req.getAdmins(); if (admins == null) { - admins = Collections.emptyList(); + admins = Collections.emptySet(); } - appService.createAppAndAddRolePermission(app, new HashSet<>(admins)); + return OpenApiModelConverters.fromApp(appService.createAppAndAddRolePermission(app, admins)); } @Override - public List getEnvClusterInfo(String appId) { + public List getEnvClusters(String appId) { List envClusters = new LinkedList<>(); List envs = portalSettings.getActiveEnvs(); @@ -134,6 +134,7 @@ public List getAuthorizedApps() { /** * Updating Application Information - Using OpenAPI DTOs + * * @param openAppDTO OpenAPI application DTO */ @Override @@ -145,11 +146,12 @@ public void updateApp(OpenAppDTO openAppDTO) { /** * Get the current user's app list (paginated) + * * @param page Pagination parameter * @return App list */ @Override - public List getAppsBySelf(Set appIds, Integer page, Integer size) { + public List getAppsWithPageAndSize(Set appIds, Integer page, Integer size) { int pageIndex = page == null ? 0 : page; int pageSize = (size == null || size <= 0) ? 20 : size; Pageable pageable = Pageable.ofSize(pageSize).withPage(pageIndex); @@ -163,12 +165,12 @@ public List getAppsBySelf(Set appIds, Integer page, Integer /** * Create an application in a specified environment + * * @param env Environment * @param app Application information - * @param operator Operator */ @Override - public void createAppInEnv(String env, OpenAppDTO app, String operator) { + public void createAppInEnv(String env, OpenAppDTO app) { if (env == null) { throw BadRequestException.invalidEnvFormat("null"); } @@ -179,14 +181,16 @@ public void createAppInEnv(String env, OpenAppDTO app, String operator) { throw BadRequestException.invalidEnvFormat(env); } App appEntity = convert(app); - appService.createAppInRemote(envEnum, appEntity); + appService.createAppInRemoteNew(envEnum, appEntity); roleInitializationService.initNamespaceSpecificEnvRoles(appEntity.getAppId(), - ConfigConsts.NAMESPACE_APPLICATION, env, operator); + ConfigConsts.NAMESPACE_APPLICATION, env, + UserIdentityContextHolder.getOperator().getUserId()); } /** * Delete an application + * * @param appId application ID * @return the deleted application */ @@ -199,56 +203,57 @@ public OpenAppDTO deleteApp(String appId) { /** * Find missing environments + * * @param appId application ID * @return list of missing environments */ - public MultiResponseEntity findMissEnvs(String appId) { - List entities = new ArrayList<>(); - MultiResponseEntity response = new MultiResponseEntity(HttpStatus.OK.value(), entities); + public List findMissEnvs(String appId) { + List missEnvs = new ArrayList<>(); + for (Env env : portalSettings.getActiveEnvs()) { try { appService.load(env, appId); } catch (Exception e) { - RichResponseEntity entity; + OpenMissEnvDTO missEnv = new OpenMissEnvDTO(); if (e instanceof HttpClientErrorException && ((HttpClientErrorException) e).getStatusCode() == HttpStatus.NOT_FOUND) { - entity = new RichResponseEntity(HttpStatus.OK.value(), HttpStatus.OK.getReasonPhrase()); - entity.setBody(env.toString()); + missEnv.setCode(HttpStatus.OK.value()); + missEnv.setMessage(env.toString()); } else { - entity = new RichResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR.value(), - "load env:" + env.getName() + " cluster error." + e.getMessage()); + missEnv.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); + missEnv.setMessage( + String.format("load appId:%s from env %s error.", appId, env) + e.getMessage()); } - response.addEntitiesItem(entity); + missEnvs.add(missEnv); } } - return response; + return missEnvs; } /** * Find AppNavTree + * * @param appId * @return list of EnvClusterInfos */ @Override - public MultiResponseEntity getAppNavTree(String appId) { - List entities = new ArrayList<>(); - MultiResponseEntity response = new MultiResponseEntity(HttpStatus.OK.value(), entities); + public List getEnvClusterInfos(String appId) { + List envClusterInfos = new ArrayList<>(); List envs = portalSettings.getActiveEnvs(); for (Env env : envs) { try { OpenEnvClusterInfo openEnvClusterInfo = OpenApiModelConverters.fromEnvClusterInfo(appService.createEnvNavNode(env, appId)); - RichResponseEntity entity = - new RichResponseEntity(HttpStatus.OK.value(), HttpStatus.OK.getReasonPhrase()); - entity.setBody(openEnvClusterInfo); - response.addEntitiesItem(entity); + openEnvClusterInfo.setCode(HttpStatus.OK.value()); + envClusterInfos.add(openEnvClusterInfo); } catch (Exception e) { - logger.warn("Failed to load env {} navigation for app {}", env, appId, e); - RichResponseEntity entity = new RichResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR.value(), - "load env:" + env.getName() + " cluster error." + e.getMessage()); - response.addEntitiesItem(entity); + OpenEnvClusterInfo envClusterInfo = new OpenEnvClusterInfo(); + envClusterInfo.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); + envClusterInfo.setEnv(env.getName()); + envClusterInfo.setMessage("load env:" + env.getName() + " cluster error." + e.getMessage()); + envClusterInfos.add(envClusterInfo); } } - return response; + return envClusterInfos; } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerInstanceOpenApiService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerInstanceOpenApiService.java index 2ccc6952ae9..bb209780de5 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerInstanceOpenApiService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerInstanceOpenApiService.java @@ -16,11 +16,22 @@ */ package com.ctrip.framework.apollo.openapi.server.service; -import com.ctrip.framework.apollo.openapi.api.InstanceOpenApiService; +import com.ctrip.framework.apollo.common.dto.InstanceConfigDTO; +import com.ctrip.framework.apollo.common.dto.InstanceDTO; +import com.ctrip.framework.apollo.common.dto.PageDTO; +import com.ctrip.framework.apollo.openapi.model.OpenInstanceDTO; +import com.ctrip.framework.apollo.openapi.model.OpenInstancePageDTO; +import com.ctrip.framework.apollo.openapi.util.OpenApiModelConverters; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.service.InstanceService; +import java.util.Collections; +import java.util.Date; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; +import java.util.List; +import java.util.Set; + @Service public class ServerInstanceOpenApiService implements InstanceOpenApiService { @@ -36,4 +47,67 @@ public int getInstanceCountByNamespace(String appId, String env, String clusterN return instanceService.getInstanceCountByNamespace(appId, Env.valueOf(env), clusterName, namespaceName); } + + /** + * Query instances by release version (supports pagination) - returns OpenAPI DTO + */ + @Override + public OpenInstancePageDTO getByRelease(String env, long releaseId, int page, int size) { + PageDTO portalPageDTO = + instanceService.getByRelease(Env.valueOf(env), releaseId, page, size); + // PageDTO portalPageDTO = mockPortalPageDTO(page, size); + + return transformToOpenPageDTO(portalPageDTO); + } + + /** + * Query instances not in a specified release - returns an OpenAPI DTO + */ + @Override + public List getByReleasesNotIn(String env, String appId, String clusterName, + String namespaceName, Set releaseIds) { + List portalInstances = instanceService.getByReleasesNotIn(Env.valueOf(env), appId, + clusterName, namespaceName, releaseIds); + return OpenApiModelConverters.fromInstanceDTOs(portalInstances); + } + + @Override + public OpenInstancePageDTO getByNamespace(String env, String appId, String clusterName, + String namespaceName, String instanceAppId, Integer page, Integer size) { + // return transformToOpenPageDTO((mockPortalPageDTO(page,size))); + return transformToOpenPageDTO(instanceService.getByNamespace(Env.valueOf(env), appId, + clusterName, namespaceName, instanceAppId, page, size)); + } + + /** + * Convert PageDTO to OpenPageDTOOpenInstanceDTO + */ + private OpenInstancePageDTO transformToOpenPageDTO(PageDTO pageDTO) { + List instances = OpenApiModelConverters.fromInstanceDTOs(pageDTO.getContent()); + OpenInstancePageDTO openInstancePageDTO = new OpenInstancePageDTO(); + openInstancePageDTO.setPage(pageDTO.getPage()); + openInstancePageDTO.setSize(pageDTO.getSize()); + openInstancePageDTO.setTotal(pageDTO.getTotal()); + openInstancePageDTO.setInstances(instances); + + return openInstancePageDTO; + } + + private PageDTO mockPortalPageDTO(int page, int size) { + InstanceConfigDTO config = new InstanceConfigDTO(); + config.setReleaseDeliveryTime(new Date()); + config.setDataChangeLastModifiedTime(new Date()); + + InstanceDTO instance = new InstanceDTO(); + instance.setId(1L); + instance.setAppId("mock-app"); + instance.setClusterName("default"); + instance.setDataCenter("SHA"); + instance.setIp("10.0.0.1"); + instance.setConfigs(Collections.singletonList(config)); + + return new PageDTO<>(Collections.singletonList(instance), PageRequest.of(page, size), 1 // mock + // 总数 + ); + } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiModelConverters.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiModelConverters.java index cc0f2eeb723..c1f6df0831c 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiModelConverters.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiModelConverters.java @@ -56,6 +56,7 @@ import com.ctrip.framework.apollo.portal.entity.vo.ItemDiffs; import com.ctrip.framework.apollo.portal.entity.vo.NamespaceIdentifier; import com.ctrip.framework.apollo.portal.entity.vo.Organization; +import com.ctrip.framework.apollo.portal.environment.Env; import com.google.common.base.Preconditions; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; @@ -72,8 +73,8 @@ import java.util.stream.Collectors; /** - * Non-invasive converters for OpenAPI generated model classes. - * This class mirrors/OpenApiBeanUtils functions but targets com.ctrip.framework.apollo.openapi.model.* types. + * Non-invasive converters for OpenAPI generated model classes. This class mirrors/OpenApiBeanUtils + * functions but targets com.ctrip.framework.apollo.openapi.model.* types. */ public final class OpenApiModelConverters { @@ -397,7 +398,17 @@ public static List fromInstanceDTOs(final List ins // newly added public static OpenEnvClusterInfo fromEnvClusterInfo(final EnvClusterInfo envClusterInfo) { Preconditions.checkArgument(envClusterInfo != null); - return BeanUtils.transform(OpenEnvClusterInfo.class, envClusterInfo); + OpenEnvClusterInfo openEnvClusterInfo = new OpenEnvClusterInfo(); + Env env = envClusterInfo.getEnv(); + if (env != null) { + openEnvClusterInfo.setEnv(env.toString()); + } + List clusters = envClusterInfo.getClusters(); + if (!CollectionUtils.isEmpty(clusters)) { + openEnvClusterInfo.setClusters(clusters.stream().map(OpenApiModelConverters::fromClusterDTO) + .collect(Collectors.toList())); + } + return openEnvClusterInfo; } // newly added diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/AppController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/AppController.java index 0e9f10b005b..98789f1ccc3 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/AppController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/AppController.java @@ -20,15 +20,25 @@ import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.openapi.api.AppManagementApi; -import com.ctrip.framework.apollo.openapi.model.MultiResponseEntity; import com.ctrip.framework.apollo.openapi.model.OpenAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenCreateAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterDTO; +import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterInfo; +import com.ctrip.framework.apollo.openapi.model.OpenMissEnvDTO; import com.ctrip.framework.apollo.openapi.server.service.AppOpenApiService; import com.ctrip.framework.apollo.openapi.service.ConsumerService; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; +import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; +import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; +import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.model.AppModel; +import com.ctrip.framework.apollo.portal.entity.po.Role; +import com.ctrip.framework.apollo.portal.service.RolePermissionService; +import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; +import com.ctrip.framework.apollo.portal.util.RoleUtils; +import java.util.HashMap; +import java.util.HashSet; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.StringUtils; @@ -49,14 +59,19 @@ public class AppController implements AppManagementApi { private final ConsumerService consumerService; private final AppOpenApiService appOpenApiService; private final UserService userService; + private final RolePermissionService rolePermissionService; + private final UserInfoHolder userInfoHolder; public AppController(final ConsumerAuthUtil consumerAuthUtil, final ConsumerService consumerService, final AppOpenApiService appOpenApiService, - final UserService userService) { + final UserService userService, final RolePermissionService rolePermissionService, + final UserInfoHolder userInfoHolder) { this.consumerAuthUtil = consumerAuthUtil; this.consumerService = consumerService; this.appOpenApiService = appOpenApiService; this.userService = userService; + this.rolePermissionService = rolePermissionService; + this.userInfoHolder = userInfoHolder; } /** @@ -65,7 +80,7 @@ public AppController(final ConsumerAuthUtil consumerAuthUtil, @Transactional @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateApplicationPermission()") @Override - public ResponseEntity createApp(OpenCreateAppDTO req) { + public ResponseEntity createApp(OpenCreateAppDTO req) { if (null == req.getApp()) { throw new BadRequestException("App is null"); } @@ -73,18 +88,25 @@ public ResponseEntity createApp(OpenCreateAppDTO req) { if (null == app.getAppId()) { throw new BadRequestException("AppId is null"); } + String operator = + app.getDataChangeCreatedBy() == null ? app.getOwnerName() : app.getDataChangeCreatedBy(); + if (userService.findByUserId(operator) == null) { + throw new BadRequestException("operator missing or not exist: " + operator); + } + UserIdentityContextHolder.setOperator(new UserInfo(operator)); // create app - this.appOpenApiService.createApp(req); - if (Boolean.TRUE.equals(req.getAssignAppRoleToSelf())) { + OpenAppDTO openAppDTO = this.appOpenApiService.createApp(req); + if (Boolean.TRUE.equals(req.getAssignAppRoleToSelf()) + && UserIdentityConstants.CONSUMER.equals(UserIdentityContextHolder.getAuthType())) { long consumerId = this.consumerAuthUtil.retrieveConsumerIdFromCtx(); consumerService.assignAppRoleToConsumer(consumerId, app.getAppId()); } - return ResponseEntity.ok().build(); + return ResponseEntity.ok(openAppDTO); } @Override - public ResponseEntity> getEnvClusterInfo(String appId) { - return ResponseEntity.ok(appOpenApiService.getEnvClusterInfo(appId)); + public ResponseEntity> getEnvClusters(String appId) { + return ResponseEntity.ok(appOpenApiService.getEnvClusters(appId)); } @Override @@ -114,11 +136,22 @@ public ResponseEntity> findAppsAuthorized() { */ @Override public ResponseEntity getApp(String appId) { + if (UserIdentityConstants.CONSUMER.equals(UserIdentityContextHolder.getAuthType())) { + // to ensure this app is authorized to this consumer + long consumerId = this.consumerAuthUtil.retrieveConsumerIdFromCtx(); + Set appIds = this.consumerService.findAppIdsAuthorizedByConsumerId(consumerId); + if (!appIds.contains(appId)) { + throw new BadRequestException("Trying to access unauthorized app: " + appId); + } + } List apps = appOpenApiService.getAppsInfo(Collections.singletonList(appId)); if (null == apps || apps.isEmpty()) { throw new BadRequestException("App not found: " + appId); } - return ResponseEntity.ok(apps.get(0)); + OpenAppDTO result = apps.get(0); + result.setOwnerDisplayName(result.getOwnerName()); + + return ResponseEntity.ok(result); } /** @@ -127,16 +160,19 @@ public ResponseEntity getApp(String appId) { @Override @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") @ApolloAuditLog(type = OpType.UPDATE, name = "App.update") - public ResponseEntity updateApp(String appId, String operator, OpenAppDTO dto) { + public ResponseEntity updateApp(String appId, OpenAppDTO dto, String operator) { if (!Objects.equals(appId, dto.getAppId())) { throw new BadRequestException("The App Id of path variable and request body is different"); } - if (userService.findByUserId(operator) == null) { - throw BadRequestException.userNotExists(operator); + if (UserIdentityConstants.CONSUMER.equals(UserIdentityContextHolder.getAuthType())) { + if (userService.findByUserId(operator) == null) { + throw BadRequestException.userNotExists(operator); + } + UserIdentityContextHolder.setOperator(new UserInfo(operator)); } appOpenApiService.updateApp(dto); - return ResponseEntity.ok(dto); + return ResponseEntity.ok(new HashMap<>()); } /** @@ -144,27 +180,42 @@ public ResponseEntity updateApp(String appId, String operator, OpenA */ @Override public ResponseEntity> getAppsBySelf(Integer page, Integer size) { - long consumerId = this.consumerAuthUtil.retrieveConsumerIdFromCtx(); - Set authorizedAppIds = - this.consumerService.findAppIdsAuthorizedByConsumerId(consumerId); - List apps = appOpenApiService.getAppsBySelf(authorizedAppIds, page, size); + Set appIds = new HashSet<>(); + if (UserIdentityConstants.CONSUMER.equals(UserIdentityContextHolder.getAuthType())) { + long consumerId = this.consumerAuthUtil.retrieveConsumerIdFromCtx(); + appIds = this.consumerService.findAppIdsAuthorizedByConsumerId(consumerId); + } else { + String userId = userInfoHolder.getUser().getUserId(); + List userRoles = rolePermissionService.findUserRoles(userId); + + for (Role role : userRoles) { + String appId = RoleUtils.extractAppIdFromRoleName(role.getRoleName()); + if (appId != null) { + appIds.add(appId); + } + } + } + + List apps = appOpenApiService.getAppsWithPageAndSize(appIds, page, size); return ResponseEntity.ok(apps); } /** - * Create an application in a specified environment (new added) - * POST /openapi/v1/apps/envs/{env} + * Create an application in a specified environment (new added) POST /openapi/v1/apps/envs/{env} */ @Override @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateApplicationPermission()") @ApolloAuditLog(type = OpType.CREATE, name = "App.create.forEnv") - public ResponseEntity createAppInEnv(String env, String operator, OpenAppDTO app) { - if (userService.findByUserId(operator) == null) { - throw BadRequestException.userNotExists(operator); + public ResponseEntity createAppInEnv(String env, OpenAppDTO app, String operator) { + if (UserIdentityConstants.CONSUMER.equals(UserIdentityContextHolder.getAuthType())) { + if (userService.findByUserId(operator) == null) { + throw BadRequestException.userNotExists(operator); + } + UserIdentityContextHolder.setOperator(new UserInfo(operator)); } - appOpenApiService.createAppInEnv(env, app, operator); + appOpenApiService.createAppInEnv(env, app); - return ResponseEntity.ok().build(); + return ResponseEntity.ok(new HashMap()); } /** @@ -172,20 +223,23 @@ public ResponseEntity createAppInEnv(String env, String operator, OpenAp */ @Override @PreAuthorize(value = "@unifiedPermissionValidator.isAppAdmin(#appId)") - @ApolloAuditLog(type = OpType.DELETE, name = "App.delete") + @ApolloAuditLog(type = OpType.RPC, name = "App.delete") public ResponseEntity deleteApp(String appId, String operator) { - if (userService.findByUserId(operator) == null) { - throw BadRequestException.userNotExists(operator); + if (UserIdentityConstants.CONSUMER.equals(UserIdentityContextHolder.getAuthType())) { + if (userService.findByUserId(operator) == null) { + throw BadRequestException.userNotExists(operator); + } + UserIdentityContextHolder.setOperator(new UserInfo(operator)); } appOpenApiService.deleteApp(appId); - return ResponseEntity.ok().build(); + return ResponseEntity.ok(new HashMap()); } /** * Find miss env (new added) */ @Override - public ResponseEntity findMissEnvs(String appId) { + public ResponseEntity> findMissEnvs(String appId) { return ResponseEntity.ok(appOpenApiService.findMissEnvs(appId)); } @@ -193,7 +247,7 @@ public ResponseEntity findMissEnvs(String appId) { * Find appNavTree (new added) */ @Override - public ResponseEntity getAppNavTree(String appId) { - return ResponseEntity.ok(appOpenApiService.getAppNavTree(appId)); + public ResponseEntity> getEnvClusterInfo(String appId) { + return ResponseEntity.ok(appOpenApiService.getEnvClusterInfos(appId)); } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/InstanceController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/InstanceController.java index 4629f5b327d..856c9f05d96 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/InstanceController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/InstanceController.java @@ -16,22 +16,77 @@ */ package com.ctrip.framework.apollo.openapi.v1.controller; -import com.ctrip.framework.apollo.openapi.api.InstanceOpenApiService; +import com.ctrip.framework.apollo.common.exception.BadRequestException; +import com.ctrip.framework.apollo.openapi.api.InstanceManagementApi; +import com.ctrip.framework.apollo.openapi.model.OpenInstanceDTO; +import com.ctrip.framework.apollo.openapi.model.OpenInstancePageDTO; +import com.ctrip.framework.apollo.openapi.server.service.InstanceOpenApiService; +import com.google.common.base.Splitter; +import org.springframework.http.ResponseEntity; +import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + @RestController("openapiInstanceController") -@RequestMapping("/openapi/v1/envs/{env}") -public class InstanceController { +public class InstanceController implements InstanceManagementApi { + private final InstanceOpenApiService instanceOpenApiService; public InstanceController(InstanceOpenApiService instanceOpenApiService) { this.instanceOpenApiService = instanceOpenApiService; } - @GetMapping(value = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/instances") - public int getInstanceCountByNamespace(@PathVariable String appId, @PathVariable String env, - @PathVariable String clusterName, @PathVariable String namespaceName) { - return this.instanceOpenApiService.getInstanceCountByNamespace(appId, env, clusterName, - namespaceName); + @Override + public ResponseEntity getInstanceCountByNamespace(String env, String appId, + String clusterName, String namespaceName) { + return ResponseEntity.ok(this.instanceOpenApiService.getInstanceCountByNamespace(appId, env, + clusterName, namespaceName)); + } + + /** + * Query instances by release version (supports pagination) + */ + @Override + public ResponseEntity getByRelease(String env, Long releaseId, Integer page, + Integer size) { + return ResponseEntity.ok(this.instanceOpenApiService.getByRelease(env, releaseId, page, size)); + } + + /** + * Query instance by namespace (supports pagination) + */ + @Override + public ResponseEntity getByNamespace(String env, String appId, + String clusterName, String namespaceName, Integer page, Integer size, String instanceAppId) { + return ResponseEntity.ok(instanceOpenApiService.getByNamespace(env, appId, clusterName, + namespaceName, instanceAppId, page, size)); + } + + /** + * Query instances not in a specified release + */ + @Override + public ResponseEntity> getByReleasesAndNamespaceNotIn(String env, + String appId, String clusterName, String namespaceName, String releaseIds) { + List rawReleaseIds = + Splitter.on(",").omitEmptyStrings().trimResults().splitToList(releaseIds); + + if (CollectionUtils.isEmpty(rawReleaseIds)) { + throw new BadRequestException("excludeReleases parameter cannot be empty"); + } + + final Set releaseIdSet; + try { + releaseIdSet = rawReleaseIds.stream().map(Long::parseLong).collect(Collectors.toSet()); + } catch (NumberFormatException ex) { + throw new BadRequestException( + "excludeReleases parameter must contain only numeric release ids", ex); + } + + return ResponseEntity.ok(this.instanceOpenApiService.getByReleasesNotIn(env, appId, clusterName, + namespaceName, releaseIdSet)); } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserIdentityContextHolder.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserIdentityContextHolder.java index 857962e8225..6d2e0d2f53c 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserIdentityContextHolder.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserIdentityContextHolder.java @@ -16,10 +16,14 @@ */ package com.ctrip.framework.apollo.portal.component; +import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; + public final class UserIdentityContextHolder { private static final ThreadLocal AUTH_TYPE_HOLDER = new ThreadLocal<>(); + private static final ThreadLocal OPERATOR_HOLDER = new ThreadLocal<>(); + private UserIdentityContextHolder() { // Prevent instantiation } @@ -38,10 +42,25 @@ public static void setAuthType(String authType) { AUTH_TYPE_HOLDER.set(authType); } + /** + * Read operator for current_thread + */ + public static UserInfo getOperator() { + return OPERATOR_HOLDER.get(); + } + + /** + * Write operator for current thread + */ + public static void setOperator(UserInfo userInfo) { + OPERATOR_HOLDER.set(userInfo); + } + /** * Clean up current thread variable to prevent memory leaks */ public static void clear() { AUTH_TYPE_HOLDER.remove(); + OPERATOR_HOLDER.remove(); } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/filter/PortalUserSessionFilter.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/filter/PortalUserSessionFilter.java index c682dcb3907..067004c9515 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/filter/PortalUserSessionFilter.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/filter/PortalUserSessionFilter.java @@ -16,6 +16,8 @@ */ package com.ctrip.framework.apollo.portal.filter; +import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; +import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import java.io.IOException; import java.util.Arrays; @@ -56,9 +58,11 @@ public class PortalUserSessionFilter implements Filter { new LoginUrlAuthenticationEntryPoint("/signin"); private final Environment environment; + private final UserInfoHolder userInfoHolder; - public PortalUserSessionFilter(Environment environment) { + public PortalUserSessionFilter(Environment environment, UserInfoHolder userInfoHolder) { this.environment = environment; + this.userInfoHolder = userInfoHolder; } @Override @@ -77,7 +81,18 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain // Portal user is authenticated, allow access to OpenAPI logger.debug("Authenticated portal user accessing OpenAPI: {}", request.getRequestURI()); request.setAttribute(PORTAL_USER_AUTHENTICATED, true); - chain.doFilter(req, resp); + if (request.getParameterMap().containsKey("operator") + || request.getAttribute("operator") != null) { + // a portal call with operator param in the query should add the operator to the current + // thread + UserIdentityContextHolder.setOperator(userInfoHolder.getUser()); + } + try { + chain.doFilter(req, resp); + } finally { + // avoid memory leaking + UserIdentityContextHolder.clear(); + } return; } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppService.java index d30b6c2cf4d..2268f9e11bb 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/AppService.java @@ -28,6 +28,7 @@ import com.ctrip.framework.apollo.core.utils.StringUtils; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI.AppAPI; import com.ctrip.framework.apollo.portal.component.PortalSettings; +import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.environment.Env; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.constant.TracerEventType; @@ -128,6 +129,23 @@ public AppDTO load(Env env, String appId) { return appAPI.loadApp(env, appId); } + // TODO: should remove this after full portal migration procedure + // this is avoiding confliction of two username fetch way, cuz createAppInRemote is still being + // used in ConfigsImportService + public void createAppInRemoteNew(Env env, App app) { + if (StringUtils.isBlank(app.getDataChangeCreatedBy())) { + String username = UserIdentityContextHolder.getOperator().getUserId(); + app.setDataChangeCreatedBy(username); + app.setDataChangeLastModifiedBy(username); + } + + AppDTO appDTO = BeanUtils.transform(AppDTO.class, app); + appAPI.createApp(env, appDTO); + + roleInitializationService.initClusterNamespaceRoles(app.getAppId(), env.getName(), + ConfigConsts.CLUSTER_NAME_DEFAULT, UserIdentityContextHolder.getOperator().getUserId()); + } + public void createAppInRemote(Env env, App app) { if (StringUtils.isBlank(app.getDataChangeCreatedBy())) { String username = userInfoHolder.getUser().getUserId(); @@ -156,7 +174,7 @@ private App createAppInLocal(App app) { } app.setOwnerEmail(owner.getEmail()); - String operator = userInfoHolder.getUser().getUserId(); + String operator = UserIdentityContextHolder.getOperator().getUserId(); app.setDataChangeCreatedBy(operator); app.setDataChangeLastModifiedBy(operator); @@ -167,7 +185,7 @@ private App createAppInLocal(App app) { List envs = portalSettings.getActiveEnvs(); for (Env env : envs) { roleInitializationService.initClusterNamespaceRoles(appId, env.getName(), - ConfigConsts.CLUSTER_NAME_DEFAULT, userInfoHolder.getUser().getUserId()); + ConfigConsts.CLUSTER_NAME_DEFAULT, UserIdentityContextHolder.getOperator().getUserId()); } Tracer.logEvent(TracerEventType.CREATE_APP, appId); @@ -185,7 +203,7 @@ public App createAppAndAddRolePermission(App app, Set admins) { if (!CollectionUtils.isEmpty(admins)) { rolePermissionService.assignRoleToUsers( RoleUtils.buildAppMasterRoleName(createdApp.getAppId()), admins, - userInfoHolder.getUser().getUserId()); + UserIdentityContextHolder.getOperator().getUserId()); } return createdApp; @@ -232,7 +250,7 @@ public App updateAppInLocal(App app) { managedApp.setOwnerName(owner.getUserId()); managedApp.setOwnerEmail(owner.getEmail()); - String operator = userInfoHolder.getUser().getUserId(); + String operator = UserIdentityContextHolder.getOperator().getUserId(); managedApp.setDataChangeLastModifiedBy(operator); return appRepository.save(managedApp); @@ -251,7 +269,7 @@ public App deleteAppInLocal(String appId) { if (managedApp == null) { throw BadRequestException.appNotExists(appId); } - String operator = userInfoHolder.getUser().getUserId(); + String operator = UserIdentityContextHolder.getOperator().getUserId(); // this operator is passed to // com.ctrip.framework.apollo.portal.listener.DeletionListener.onAppDeletionEvent diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthFilterConfiguration.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthFilterConfiguration.java index b5dc93209eb..f76f21baf89 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthFilterConfiguration.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthFilterConfiguration.java @@ -21,6 +21,7 @@ import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; import com.ctrip.framework.apollo.portal.filter.PortalUserSessionFilter; import com.ctrip.framework.apollo.portal.filter.UserTypeResolverFilter; +import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -64,13 +65,13 @@ public FilterRegistrationBean authTypeResolverFilter() { */ @Bean public FilterRegistrationBean portalUserSessionFilter( - Environment environment) { + Environment environment, UserInfoHolder userInfoHolder) { FilterRegistrationBean filter = new FilterRegistrationBean<>(); - filter.setFilter(new PortalUserSessionFilter(environment)); + filter.setFilter(new PortalUserSessionFilter(environment, userInfoHolder)); filter.addUrlPatterns("/openapi/*"); filter.setOrder(OPEN_API_AUTH_ORDER - 1); // Run before ConsumerAuthenticationFilter after - // springSecurityFilterChain + // springSecurityFilterChain return filter; } diff --git a/apollo-portal/src/main/resources/static/scripts/AppUtils.js b/apollo-portal/src/main/resources/static/scripts/AppUtils.js index d856f72c3b9..4ef5a43016e 100644 --- a/apollo-portal/src/main/resources/static/scripts/AppUtils.js +++ b/apollo-portal/src/main/resources/static/scripts/AppUtils.js @@ -94,6 +94,17 @@ appUtil.service('AppUtil', ['toastr', '$window', '$q', '$translate', 'prefixLoca }); return result; }, + gatherData: function (response) { + var data = []; + response.forEach(function (entity) { + if (entity.code === 200) { + data.push(entity); + } else { + toastr.warning(entity.message); + } + }); + return data; + }, collectData: function (response) { var data = []; response.entities.forEach(function (entity) { diff --git a/apollo-portal/src/main/resources/static/scripts/controller/AppController.js b/apollo-portal/src/main/resources/static/scripts/controller/AppController.js index c2712557e18..3a849499a54 100644 --- a/apollo-portal/src/main/resources/static/scripts/controller/AppController.js +++ b/apollo-portal/src/main/resources/static/scripts/controller/AppController.js @@ -108,7 +108,19 @@ function createAppController($scope, $window, $translate, toastr, AppService, Ap }) } - AppService.create($scope.app).then(function (result) { + var openCreateAppDTO = { + assignAppRoleToSelf: false, + admins: $scope.app.admins || [], + app: { + name: $scope.app.name, + appId: $scope.app.appId, + orgId: $scope.app.orgId, + orgName: $scope.app.orgName, + ownerName: $scope.app.ownerName + } + }; + + AppService.create(openCreateAppDTO).then(function (result) { toastr.success($translate.instant('Common.Created')); setInterval(function () { $scope.submitBtnDisabled = false; diff --git a/apollo-portal/src/main/resources/static/scripts/controller/ManageClusterController.js b/apollo-portal/src/main/resources/static/scripts/controller/ManageClusterController.js index 7656bf3aa93..f1fda5ffb08 100644 --- a/apollo-portal/src/main/resources/static/scripts/controller/ManageClusterController.js +++ b/apollo-portal/src/main/resources/static/scripts/controller/ManageClusterController.js @@ -27,7 +27,7 @@ manage_cluster_module.controller('ManageClusterController', function loadClusters() { AppService.load_nav_tree($scope.appId).then(function (result) { - var nodes = AppUtil.collectData(result); + var nodes = AppUtil.gatherData(result); if (!nodes || nodes.length == 0) { toastr.error($translate.instant('Config.SystemError')); return; diff --git a/apollo-portal/src/main/resources/static/scripts/controller/config/ConfigBaseInfoController.js b/apollo-portal/src/main/resources/static/scripts/controller/config/ConfigBaseInfoController.js index 59f7f30d84d..ecb68bc0ef3 100644 --- a/apollo-portal/src/main/resources/static/scripts/controller/config/ConfigBaseInfoController.js +++ b/apollo-portal/src/main/resources/static/scripts/controller/config/ConfigBaseInfoController.js @@ -122,7 +122,7 @@ function ConfigBaseInfoController($rootScope, $scope, $window, $location, $trans function findMissEnvs() { $scope.missEnvs = []; AppService.find_miss_envs($rootScope.pageContext.appId).then(function (result) { - $scope.missEnvs = AppUtil.collectData(result); + $scope.missEnvs = AppUtil.gatherData(result); if ($scope.missEnvs.length > 0) { toastr.warning($translate.instant('Config.ProjectMissEnvInfos')); @@ -192,7 +192,7 @@ function ConfigBaseInfoController($rootScope, $scope, $window, $location, $trans AppService.load_nav_tree($rootScope.pageContext.appId).then(function (result) { var navTree = []; - var nodes = AppUtil.collectData(result); + var nodes = AppUtil.gatherData(result); if (!nodes || nodes.length == 0) { toastr.error($translate.instant('Config.SystemError')); diff --git a/apollo-portal/src/main/resources/static/scripts/directive/directive.js b/apollo-portal/src/main/resources/static/scripts/directive/directive.js index 00027e26140..2333a4d68ed 100644 --- a/apollo-portal/src/main/resources/static/scripts/directive/directive.js +++ b/apollo-portal/src/main/resources/static/scripts/directive/directive.js @@ -165,7 +165,7 @@ directive_module.directive('apolloclusterselector', function ($compile, $window, function refreshClusterList() { AppService.load_nav_tree(scope.appId).then(function (result) { scope.clusters = []; - var envClusterInfo = AppUtil.collectData(result); + var envClusterInfo = AppUtil.gatherData(result); envClusterInfo.forEach(function (node) { var env = node.env; node.clusters.forEach(function (cluster) { diff --git a/apollo-portal/src/main/resources/static/scripts/services/AppService.js b/apollo-portal/src/main/resources/static/scripts/services/AppService.js index 00d6297ca1d..082899f80d4 100644 --- a/apollo-portal/src/main/resources/static/scripts/services/AppService.js +++ b/apollo-portal/src/main/resources/static/scripts/services/AppService.js @@ -19,37 +19,39 @@ appService.service('AppService', ['$resource', '$q', 'AppUtil', function ($resou find_apps: { method: 'GET', isArray: true, - url: AppUtil.prefixPath() + '/apps' + url: AppUtil.prefixPath() + '/openapi/v1/apps' }, find_app_by_self: { method: 'GET', isArray: true, - url: AppUtil.prefixPath() + '/apps/by-self' + url: AppUtil.prefixPath() + '/openapi/v1/apps/by-self' }, load_navtree: { method: 'GET', - isArray: false, - url: AppUtil.prefixPath() + '/apps/:appId/navtree' + isArray: true, + url: AppUtil.prefixPath() + '/openapi/v1/apps/:appId/env-cluster-info' }, load_app: { method: 'GET', - isArray: false + isArray: false, + url: AppUtil.prefixPath() + '/openapi/v1/apps/:appId' }, create_app: { method: 'POST', - url: AppUtil.prefixPath() + '/apps' + url: AppUtil.prefixPath() + '/openapi/v1/apps' }, update_app: { method: 'PUT', - url: AppUtil.prefixPath() + '/apps/:appId' + url: AppUtil.prefixPath() + '/openapi/v1/apps/:appId' }, create_app_remote: { method: 'POST', - url: AppUtil.prefixPath() + '/apps/envs/:env' + url: AppUtil.prefixPath() + '/openapi/v1/apps/envs/:env' }, find_miss_envs: { method: 'GET', - url: AppUtil.prefixPath() + '/apps/:appId/miss_envs' + isArray: true, + url: AppUtil.prefixPath() + '/openapi/v1/apps/:appId/miss-envs' }, create_missing_namespaces: { method: 'POST', @@ -61,7 +63,8 @@ appService.service('AppService', ['$resource', '$q', 'AppUtil', function ($resou }, delete_app: { method: 'DELETE', - isArray: false + isArray: false, + url: AppUtil.prefixPath() + '/openapi/v1/apps/:appId' }, allow_app_master_assign_role: { method: 'POST', @@ -124,7 +127,8 @@ appService.service('AppService', ['$resource', '$q', 'AppUtil', function ($resou update: function (app) { var d = $q.defer(); app_resource.update_app({ - appId: app.appId + appId: app.appId, + operator: '' }, app, function (result) { d.resolve(result); }, function (result) { @@ -134,7 +138,7 @@ appService.service('AppService', ['$resource', '$q', 'AppUtil', function ($resou }, create_remote: function (env, app) { var d = $q.defer(); - app_resource.create_app_remote({env: env}, app, function (result) { + app_resource.create_app_remote({env: env, operator: ''}, app, function (result) { d.resolve(result); }, function (result) { d.reject(result); @@ -192,7 +196,8 @@ appService.service('AppService', ['$resource', '$q', 'AppUtil', function ($resou delete_app: function (appId) { var d = $q.defer(); app_resource.delete_app({ - appId: appId + appId: appId, + operator: '' }, function (result) { d.resolve(result); }, function (result) { diff --git a/apollo-portal/src/main/resources/static/scripts/services/InstanceService.js b/apollo-portal/src/main/resources/static/scripts/services/InstanceService.js index e519ab56a07..51acfb7e95e 100644 --- a/apollo-portal/src/main/resources/static/scripts/services/InstanceService.js +++ b/apollo-portal/src/main/resources/static/scripts/services/InstanceService.js @@ -18,22 +18,22 @@ appService.service('InstanceService', ['$resource', '$q', 'AppUtil', function ($ var resource = $resource('', {}, { find_instances_by_release: { method: 'GET', - url: AppUtil.prefixPath() + '/envs/:env/instances/by-release' + url: AppUtil.prefixPath() + '/openapi/v1/envs/:env/instances/by-release' }, find_instances_by_namespace: { method: 'GET', isArray: false, - url: AppUtil.prefixPath() + '/envs/:env/instances/by-namespace' + url: AppUtil.prefixPath() + '/openapi/v1/envs/:env/instances/by-namespace' }, find_by_releases_not_in: { method: 'GET', isArray: true, - url: AppUtil.prefixPath() + '/envs/:env/instances/by-namespace-and-releases-not-in' + url: AppUtil.prefixPath() + '/openapi/v1/envs/:env/instances/by-namespace-and-releases-not-in' }, get_instance_count_by_namespace: { method: 'GET', isArray: false, - url: AppUtil.prefixPath() + "/envs/:env/instances/by-namespace/count" + url: AppUtil.prefixPath() + "/openapi/v1/envs/:env/apps/:appId/clusters/:clusterName/namespaces/:namespaceName/instances" } }); diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/AppControllerParamBindLowLevelTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/AppControllerParamBindLowLevelTest.java index 2399217407e..24b69c4db01 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/AppControllerParamBindLowLevelTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/AppControllerParamBindLowLevelTest.java @@ -17,27 +17,14 @@ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.openapi.model.OpenAppDTO; -import com.ctrip.framework.apollo.openapi.repository.ConsumerAuditRepository; -import com.ctrip.framework.apollo.openapi.repository.ConsumerRepository; -import com.ctrip.framework.apollo.openapi.repository.ConsumerRoleRepository; -import com.ctrip.framework.apollo.openapi.repository.ConsumerTokenRepository; import com.ctrip.framework.apollo.openapi.server.service.AppOpenApiService; import com.ctrip.framework.apollo.openapi.service.ConsumerService; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; -import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; -import com.ctrip.framework.apollo.portal.repository.PermissionRepository; -import com.ctrip.framework.apollo.portal.repository.RolePermissionRepository; -import com.ctrip.framework.apollo.portal.service.AppService; -import com.ctrip.framework.apollo.portal.service.ClusterService; -import com.ctrip.framework.apollo.portal.service.RoleInitializationService; -import com.ctrip.framework.apollo.portal.service.RolePermissionService; -import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; -import com.ctrip.framework.apollo.portal.repository.RoleRepository; import com.google.gson.Gson; import org.junit.After; import org.junit.Before; @@ -74,39 +61,13 @@ public class AppControllerParamBindLowLevelTest { @MockBean(name = "unifiedPermissionValidator") private UnifiedPermissionValidator unifiedPermissionValidator; @MockBean - private PortalSettings portalSettings; - @MockBean - private AppService appService; - @MockBean - private ClusterService clusterService; - @MockBean private ConsumerAuthUtil consumerAuthUtil; @MockBean - private PermissionRepository permissionRepository; - @MockBean private AppOpenApiService appOpenApiService; @MockBean private ConsumerService consumerService; @MockBean - private RolePermissionRepository rolePermissionRepository; - @MockBean - private UserInfoHolder userInfoHolder; - @MockBean - private ConsumerTokenRepository consumerTokenRepository; - @MockBean - private ConsumerRepository consumerRepository; - @MockBean - private ConsumerAuditRepository consumerAuditRepository; - @MockBean - private ConsumerRoleRepository consumerRoleRepository; - @MockBean - private RolePermissionService rolePermissionService; - @MockBean private UserService userService; - @MockBean - private RoleRepository roleRepository; - @MockBean - private RoleInitializationService roleInitializationService; private final Gson gson = new Gson(); @@ -156,12 +117,13 @@ public void createAppInEnv_shouldBind_env_query_body() throws Exception { ArgumentCaptor dtoCap = ArgumentCaptor.forClass(OpenAppDTO.class); ArgumentCaptor opCap = ArgumentCaptor.forClass(String.class); - verify(appOpenApiService, times(1)).createAppInEnv(envCap.capture(), dtoCap.capture(), - opCap.capture()); + verify(appOpenApiService, times(1)).createAppInEnv(envCap.capture(), dtoCap.capture()); assertThat(envCap.getValue()).isEqualTo("DEV"); - assertThat(opCap.getValue()).isEqualTo("bob"); assertThat(dtoCap.getValue().getAppId()).isEqualTo("demo"); assertThat(dtoCap.getValue().getName()).isEqualTo("demo-name"); + + verify(userService, times(1)).findByUserId(opCap.capture()); + assertThat(opCap.getValue()).isEqualTo("bob"); } @Test @@ -181,7 +143,7 @@ public void getAppsBySelf_shouldBind_page_size_and_ids() throws Exception { ArgumentCaptor pageCap = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor sizeCap = ArgumentCaptor.forClass(Integer.class); - verify(appOpenApiService, times(1)).getAppsBySelf(idsCap.capture(), pageCap.capture(), + verify(appOpenApiService, times(1)).getAppsWithPageAndSize(idsCap.capture(), pageCap.capture(), sizeCap.capture()); assertThat(idsCap.getValue()).containsExactlyInAnyOrder("app1", "app2"); assertThat(pageCap.getValue()).isEqualTo(0); @@ -211,8 +173,9 @@ public void deleteApp_shouldBind_path_and_query() throws Exception { when(appOpenApiService.deleteApp("app-1")).thenReturn(new OpenAppDTO()); mockMvc.perform(delete("/openapi/v1/apps/{appId}", "app-1") - .param("operator", "alice")) - .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk()); + .param("operator", "alice")) + .andExpect( + org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk()); verify(appOpenApiService, times(1)).deleteApp("app-1"); } diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/AppControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/AppControllerTest.java index 0e475f6a2ef..9c366005ca6 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/AppControllerTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/AppControllerTest.java @@ -16,30 +16,18 @@ */ package com.ctrip.framework.apollo.openapi.v1.controller; -import com.ctrip.framework.apollo.openapi.model.MultiResponseEntity; import com.ctrip.framework.apollo.openapi.model.OpenAppDTO; import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterDTO; -import com.ctrip.framework.apollo.openapi.repository.ConsumerAuditRepository; -import com.ctrip.framework.apollo.openapi.repository.ConsumerRepository; -import com.ctrip.framework.apollo.openapi.repository.ConsumerRoleRepository; -import com.ctrip.framework.apollo.openapi.repository.ConsumerTokenRepository; +import com.ctrip.framework.apollo.openapi.model.OpenEnvClusterInfo; +import com.ctrip.framework.apollo.openapi.model.OpenMissEnvDTO; import com.ctrip.framework.apollo.openapi.server.service.AppOpenApiService; import com.ctrip.framework.apollo.openapi.service.ConsumerService; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; -import com.ctrip.framework.apollo.portal.component.PortalSettings; import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.constant.UserIdentityConstants; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; -import com.ctrip.framework.apollo.portal.repository.PermissionRepository; -import com.ctrip.framework.apollo.portal.repository.RolePermissionRepository; -import com.ctrip.framework.apollo.portal.service.AppService; -import com.ctrip.framework.apollo.portal.service.ClusterService; -import com.ctrip.framework.apollo.portal.service.RoleInitializationService; -import com.ctrip.framework.apollo.portal.service.RolePermissionService; -import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.UserService; -import com.ctrip.framework.apollo.portal.repository.RoleRepository; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.gson.Gson; @@ -53,7 +41,6 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -65,7 +52,6 @@ import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; @@ -88,51 +74,14 @@ public class AppControllerTest { private MockMvc mockMvc; @MockBean(name = "unifiedPermissionValidator") private UnifiedPermissionValidator unifiedPermissionValidator; - - @MockBean - private PortalSettings portalSettings; - - @MockBean - private AppService appService; - - @MockBean - private ClusterService clusterService; - @MockBean private ConsumerAuthUtil consumerAuthUtil; - - @MockBean - private PermissionRepository permissionRepository; - @MockBean private AppOpenApiService appOpenApiService; - @MockBean private ConsumerService consumerService; - - @MockBean - private RolePermissionRepository rolePermissionRepository; - - @MockBean - private UserInfoHolder userInfoHolder; - @MockBean - private ConsumerTokenRepository consumerTokenRepository; - @MockBean - private ConsumerRepository consumerRepository; - @MockBean - private ConsumerAuditRepository consumerAuditRepository; - @MockBean - private ConsumerRoleRepository consumerRoleRepository; - @MockBean - private RolePermissionService rolePermissionService; @MockBean private UserService userService; - @MockBean - private RoleRepository roleRepository; - @MockBean - private RoleInitializationService roleInitializationService; - @MockBean - private ApplicationEventPublisher applicationEventPublisher; private final Gson gson = new Gson(); @@ -181,7 +130,7 @@ public void testFindAppsAuthorized() throws Exception { } @Test - public void testGetEnvClusterInfo() throws Exception { + public void testGetEnvClusters() throws Exception { String appId = "someAppId"; OpenEnvClusterDTO devCluster = new OpenEnvClusterDTO(); @@ -191,7 +140,7 @@ public void testGetEnvClusterInfo() throws Exception { fatCluster.setEnv("FAT"); fatCluster.setClusters(Lists.newArrayList("default", "feature")); - when(appOpenApiService.getEnvClusterInfo(appId)) + when(appOpenApiService.getEnvClusters(appId)) .thenReturn(Lists.newArrayList(devCluster, fatCluster)); mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/apps/" + appId + "/envclusters")) @@ -202,7 +151,7 @@ public void testGetEnvClusterInfo() throws Exception { .andExpect(MockMvcResultMatchers.jsonPath("$.[1].clusters[0]").value("default")) .andExpect(MockMvcResultMatchers.jsonPath("$.[1].clusters[1]").value("feature")); - Mockito.verify(appOpenApiService).getEnvClusterInfo(appId); + Mockito.verify(appOpenApiService).getEnvClusters(appId); } @Test @@ -254,15 +203,21 @@ public void testFindAllApps() throws Exception { @Test public void testGetApp() throws Exception { String appId = "someAppId"; + long consumerId = 11L; OpenAppDTO app = new OpenAppDTO(); app.setAppId(appId); + app.setOwnerName("apollo-owner"); + when(consumerAuthUtil.retrieveConsumerIdFromCtx()).thenReturn(consumerId); + when(consumerService.findAppIdsAuthorizedByConsumerId(consumerId)) + .thenReturn(Sets.newHashSet(appId)); when(appOpenApiService.getAppsInfo(Collections.singletonList(appId))) .thenReturn(Collections.singletonList(app)); mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/apps/" + appId)) .andExpect(MockMvcResultMatchers.status().isOk()) - .andExpect(MockMvcResultMatchers.jsonPath("$.appId").value(appId)); + .andExpect(MockMvcResultMatchers.jsonPath("$.appId").value(appId)) + .andExpect(MockMvcResultMatchers.jsonPath("$.ownerDisplayName").value("apollo-owner")); Mockito.verify(appOpenApiService).getAppsInfo(Collections.singletonList(appId)); } @@ -270,7 +225,11 @@ public void testGetApp() throws Exception { @Test public void testGetAppNotFound() throws Exception { String appId = "someAppId"; + long consumerId = 22L; + when(consumerAuthUtil.retrieveConsumerIdFromCtx()).thenReturn(consumerId); + when(consumerService.findAppIdsAuthorizedByConsumerId(consumerId)) + .thenReturn(Sets.newHashSet(appId)); when(appOpenApiService.getAppsInfo(Collections.singletonList(appId))) .thenReturn(Collections.emptyList()); @@ -280,6 +239,22 @@ public void testGetAppNotFound() throws Exception { Mockito.verify(appOpenApiService).getAppsInfo(Collections.singletonList(appId)); } + @Test + public void testGetAppUnauthorizedForConsumer() throws Exception { + String appId = "someAppId"; + long consumerId = 33L; + + when(consumerAuthUtil.retrieveConsumerIdFromCtx()).thenReturn(consumerId); + when(consumerService.findAppIdsAuthorizedByConsumerId(consumerId)) + .thenReturn(Sets.newHashSet("otherApp")); + + mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/apps/" + appId)) + .andExpect(MockMvcResultMatchers.status().isBadRequest()); + + Mockito.verify(appOpenApiService, Mockito.never()) + .getAppsInfo(Collections.singletonList(appId)); + } + @Test public void testGetAppsBySelf() throws Exception { long consumerId = 1L; @@ -299,7 +274,7 @@ public void testGetAppsBySelf() throws Exception { app2.setAppId(app2Id); List apps = Lists.newArrayList(app1, app2); - when(appOpenApiService.getAppsBySelf(authorizedAppIds, page, size)).thenReturn(apps); + when(appOpenApiService.getAppsWithPageAndSize(authorizedAppIds, page, size)).thenReturn(apps); mockMvc .perform(MockMvcRequestBuilders.get("/openapi/v1/apps/by-self") @@ -309,21 +284,43 @@ public void testGetAppsBySelf() throws Exception { .andExpect(MockMvcResultMatchers.jsonPath("$.[1].appId").value(app2Id)); Mockito.verify(this.consumerService).findAppIdsAuthorizedByConsumerId(consumerId); - Mockito.verify(this.appOpenApiService).getAppsBySelf(authorizedAppIds, page, size); + Mockito.verify(this.appOpenApiService).getAppsWithPageAndSize(authorizedAppIds, page, size); } @Test public void testFindMissEnvs() throws Exception { String appId = "someAppId"; - when(appOpenApiService.findMissEnvs(appId)) - .thenReturn(new MultiResponseEntity(HttpStatus.OK.value(), new ArrayList<>())); - mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/apps/" + appId + "/miss_envs")) - .andExpect(MockMvcResultMatchers.status().isOk()); + OpenMissEnvDTO missEnvDTO = new OpenMissEnvDTO(); + missEnvDTO.setCode(HttpStatus.OK.value()); + missEnvDTO.setMessage("DEV"); + + when(appOpenApiService.findMissEnvs(appId)).thenReturn(Collections.singletonList(missEnvDTO)); + mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/apps/" + appId + "/miss-envs")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.[0].code").value(HttpStatus.OK.value())) + .andExpect(MockMvcResultMatchers.jsonPath("$.[0].message").value("DEV")); Mockito.verify(appOpenApiService).findMissEnvs(appId); } + @Test + public void testGetEnvClusterInfos() throws Exception { + String appId = "someAppId"; + OpenEnvClusterInfo info = new OpenEnvClusterInfo(); + info.setEnv("DEV"); + info.setMessage("ok"); + + when(appOpenApiService.getEnvClusterInfos(appId)).thenReturn(Collections.singletonList(info)); + + mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/apps/" + appId + "/env-cluster-info")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.[0].env").value("DEV")) + .andExpect(MockMvcResultMatchers.jsonPath("$.[0].message").value("ok")); + + Mockito.verify(appOpenApiService).getEnvClusterInfos(appId); + } + @Test public void testUpdateApp() throws Exception { String appId = "app1"; @@ -343,10 +340,9 @@ public void testUpdateApp() throws Exception { mockMvc .perform(MockMvcRequestBuilders.put("/openapi/v1/apps/" + appId).param("operator", operator) .contentType(MediaType.APPLICATION_JSON).content(gson.toJson(requestDto))) - .andExpect(MockMvcResultMatchers.status().isOk()) - .andExpect(MockMvcResultMatchers.jsonPath("$.appId").value(appId)) - .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("App One")); + .andExpect(MockMvcResultMatchers.status().isOk()); + Mockito.verify(appOpenApiService).updateApp(Mockito.any(OpenAppDTO.class)); } @Test @@ -387,7 +383,7 @@ public void testDeleteApp() throws Exception { mockMvc.perform(delete("/openapi/v1/apps/" + appId).param("operator", operator)) .andExpect(MockMvcResultMatchers.status().isOk()) - .andExpect(MockMvcResultMatchers.content().string("")); + .andExpect(MockMvcResultMatchers.content().json("{}")); Mockito.verify(appOpenApiService).deleteApp(appId); } diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/InstanceControllerParamBindLowLevelTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/InstanceControllerParamBindLowLevelTest.java new file mode 100644 index 00000000000..69222a0c3ea --- /dev/null +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/InstanceControllerParamBindLowLevelTest.java @@ -0,0 +1,141 @@ +/* + * Copyright 2025 Apollo Authors + * + * 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.ctrip.framework.apollo.openapi.v1.controller; + +import com.ctrip.framework.apollo.openapi.auth.ConsumerPermissionValidator; +import com.ctrip.framework.apollo.openapi.model.OpenInstanceDTO; +import com.ctrip.framework.apollo.openapi.model.OpenInstancePageDTO; +import com.ctrip.framework.apollo.openapi.server.service.InstanceOpenApiService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +public class InstanceControllerParamBindLowLevelTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private InstanceOpenApiService instanceOpenApiService; + + @MockBean(name = "consumerPermissionValidator") + private ConsumerPermissionValidator consumerPermissionValidator; + + @Test + public void getInstanceCountByNamespace_shouldBindPathVariables() throws Exception { + when(instanceOpenApiService.getInstanceCountByNamespace(anyString(), anyString(), anyString(), + anyString())) + .thenReturn(15); + + mockMvc.perform( + get("/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/instances", + "FAT", "sample-app", "default", "application")) + .andExpect(status().isOk()) + .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.content() + .contentTypeCompatibleWith(MediaType.APPLICATION_JSON)); + + verify(instanceOpenApiService) + .getInstanceCountByNamespace("sample-app", "FAT", "default", "application"); + } + + @Test + public void getInstancesByRelease_shouldBindPathAndQuery() throws Exception { + OpenInstancePageDTO page = new OpenInstancePageDTO(); + page.setPage(1); + page.setSize(5); + page.setTotal(20L); + page.setInstances(Collections.singletonList(new OpenInstanceDTO())); + + when(instanceOpenApiService.getByRelease("DEV", 123L, 1, 5)).thenReturn(page); + + mockMvc.perform(get("/openapi/v1/envs/{env}/instances/by-release", "DEV") + .param("releaseId", "123").param("page", "1").param("size", "5")) + .andExpect(status().isOk()); + + verify(instanceOpenApiService).getByRelease("DEV", 123L, 1, 5); + } + + @Test + public void getInstancesExcludingReleases_shouldRejectEmptyParam() throws Exception { + mockMvc + .perform(get("/openapi/v1/envs/{env}/instances/by-namespace-and-releases-not-in", "UAT") + .param("appId", "demo-app").param("clusterName", "default") + .param("namespaceName", "application").param("releaseIds", "")) + .andExpect(status().isBadRequest()); + + verifyNoInteractions(instanceOpenApiService); + } + + @Test + public void getInstancesExcludingReleases_shouldRejectNonNumeric() throws Exception { + mockMvc + .perform(get("/openapi/v1/envs/{env}/instances/by-namespace-and-releases-not-in", "PRO") + .param("appId", "app").param("clusterName", "cluster") + .param("namespaceName", "namespace").param("releaseIds", "1,invalid,3")) + .andExpect(status().isBadRequest()); + + verifyNoInteractions(instanceOpenApiService); + } + + @Test + public void getInstancesExcludingReleases_shouldBindAllParameters() throws Exception { + when(instanceOpenApiService.getByReleasesNotIn( + anyString(), anyString(), anyString(), anyString(), anySet())) + .thenReturn(Arrays.asList(new OpenInstanceDTO(), new OpenInstanceDTO())); + + mockMvc.perform( + get("/openapi/v1/envs/{env}/instances/by-namespace-and-releases-not-in", + "PRO") + .param("appId", "bind-app") + .param("clusterName", "cluster-a") + .param("namespaceName", "namespace-x") + .param("releaseIds", "10, 11 ,12")) + .andExpect(status().isOk()); + + @SuppressWarnings("unchecked") + ArgumentCaptor> captor = ArgumentCaptor.forClass(Set.class); + verify(instanceOpenApiService) + .getByReleasesNotIn(eq("PRO"), eq("bind-app"), eq("cluster-a"), eq("namespace-x"), + captor.capture()); + + assertThat(captor.getValue()).containsExactlyInAnyOrder(10L, 11L, 12L); + } +} diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/InstanceControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/InstanceControllerTest.java new file mode 100644 index 00000000000..4a35932e9c8 --- /dev/null +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/InstanceControllerTest.java @@ -0,0 +1,157 @@ +/* + * Copyright 2025 Apollo Authors + * + * 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.ctrip.framework.apollo.openapi.v1.controller; + +import com.ctrip.framework.apollo.openapi.auth.ConsumerPermissionValidator; +import com.ctrip.framework.apollo.openapi.model.OpenInstanceDTO; +import com.ctrip.framework.apollo.openapi.model.OpenInstancePageDTO; +import com.ctrip.framework.apollo.openapi.server.service.InstanceOpenApiService; +import com.google.common.collect.Sets; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; + +import java.util.Collections; +import java.util.Set; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@TestPropertySource(properties = {"api.pool.max.total=100", "api.pool.max.per.route=100", + "api.connectionTimeToLive=30000", "api.connectTimeout=5000", "api.readTimeout=5000"}) +public class InstanceControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private InstanceOpenApiService instanceOpenApiService; + + @MockBean(name = "consumerPermissionValidator") + private ConsumerPermissionValidator consumerPermissionValidator; + + @Test + public void testGetInstanceCountByNamespace() throws Exception { + String appId = "app-id-test"; + String env = "DEV"; + String clusterName = "default"; + String namespaceName = "application"; + int mockInstanceCount = 10; + + Mockito.when( + instanceOpenApiService.getInstanceCountByNamespace(appId, env, clusterName, namespaceName)) + .thenReturn(mockInstanceCount); + + mockMvc + .perform(MockMvcRequestBuilders + .get(String.format("/openapi/v1/envs/%s/apps/%s/clusters/%s/namespaces/%s/instances", + env, appId, clusterName, namespaceName))) + .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(content().string(String.valueOf(mockInstanceCount))); + + Mockito.verify(instanceOpenApiService).getInstanceCountByNamespace(appId, env, clusterName, + namespaceName); + } + + @Test + public void testGetInstancesByRelease() throws Exception { + String env = "DEV"; + int releaseId = 100; + int page = 0; + int size = 10; + + OpenInstancePageDTO mockPage = new OpenInstancePageDTO(); + mockPage.setPage(page); + mockPage.setSize(size); + mockPage.setTotal(1L); + mockPage.setInstances(Collections.singletonList(new OpenInstanceDTO())); + + Mockito.when(instanceOpenApiService.getByRelease(env, releaseId, page, size)) + .thenReturn(mockPage); + + mockMvc + .perform(MockMvcRequestBuilders + .get(String.format("/openapi/v1/envs/%s/instances/by-release", env)) + .param("releaseId", String.valueOf(releaseId)).param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$.instances").isArray()) + .andExpect(jsonPath("$.instances[0]").exists()); + + Mockito.verify(instanceOpenApiService).getByRelease(env, releaseId, page, size); + } + + @Test + public void testGetInstancesExcludingReleases() throws Exception { + String env = "UAT"; + String appId = "another-app"; + String clusterName = "default"; + String namespaceName = "application"; + String releaseIds = "1,2,3"; + Set releaseIdSet = Sets.newHashSet(1L, 2L, 3L); + + Mockito.when(instanceOpenApiService.getByReleasesNotIn(env, appId, clusterName, namespaceName, + releaseIdSet)).thenReturn(Collections.singletonList(new OpenInstanceDTO())); + + mockMvc + .perform(MockMvcRequestBuilders + .get(String.format("/openapi/v1/envs/%s/instances/by-namespace-and-releases-not-in", + env)) + .param("appId", appId).param("clusterName", clusterName) + .param("namespaceName", namespaceName).param("releaseIds", releaseIds)) + .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()).andExpect(jsonPath("$[0]").exists()); + + Mockito.verify(instanceOpenApiService).getByReleasesNotIn(env, appId, clusterName, + namespaceName, releaseIdSet); + } + + @Test + public void testGetInstancesExcludingReleases_InvalidReleaseIds() throws Exception { + String env = "UAT"; + String appId = "another-app"; + String clusterName = "default"; + String namespaceName = "application"; + String releaseIds = "1,abc,3"; + + mockMvc + .perform(MockMvcRequestBuilders + .get(String.format("/openapi/v1/envs/%s/instances/by-namespace-and-releases-not-in", + env)) + .param("appId", appId).param("clusterName", clusterName) + .param("namespaceName", namespaceName).param("releaseIds", releaseIds)) + .andDo(MockMvcResultHandlers.print()).andExpect(status().isBadRequest()); + + Mockito.verifyNoInteractions(instanceOpenApiService); + } + +} diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/filter/PortalOpenApiAuthenticationScenariosTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/filter/PortalOpenApiAuthenticationScenariosTest.java index 1b91b606853..448901a17b2 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/filter/PortalOpenApiAuthenticationScenariosTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/filter/PortalOpenApiAuthenticationScenariosTest.java @@ -18,6 +18,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -27,8 +28,12 @@ import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; import com.ctrip.framework.apollo.openapi.util.ConsumerAuditUtil; import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil; +import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; +import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; +import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; import com.ctrip.framework.apollo.portal.spi.configuration.AuthFilterConfiguration; import java.util.Date; +import java.util.concurrent.atomic.AtomicBoolean; import javax.servlet.FilterChain; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; @@ -57,6 +62,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; @@ -144,9 +150,14 @@ public ResponseEntity loadOpenApiCluster(@PathVariable String env, @MockBean private ConsumerAuditUtil consumerAuditUtil; + @MockBean + private UserInfoHolder userInfoHolder; + @After public void tearDown() { - reset(consumerAuthUtil, consumerAuditUtil); + reset(consumerAuthUtil, consumerAuditUtil, userInfoHolder); + SecurityContextHolder.clearContext(); + UserIdentityContextHolder.clear(); } private MockHttpSession authenticatedPortalSession() { @@ -178,7 +189,9 @@ public void portalRequestWithExpiredSession_shouldRedirectToSignin() throws Exce // oidc path is handled by PortalUserSessionFilter MockEnvironment oidcEnvironment = new MockEnvironment(); oidcEnvironment.setActiveProfiles("oidc"); - PortalUserSessionFilter oidcFilter = new PortalUserSessionFilter(oidcEnvironment); + UserInfoHolder userInfoHolder = mock(UserInfoHolder.class); + PortalUserSessionFilter oidcFilter = + new PortalUserSessionFilter(oidcEnvironment, userInfoHolder); MockHttpServletRequest request = new MockHttpServletRequest("GET", PORTAL_URI); request.setCookies(new Cookie("SESSION", "expired")); @@ -217,7 +230,9 @@ public void openApiRequestWithExpiredSession_shouldFollowProfileSpecificHandling // oidc MockEnvironment oidcEnvironment = new MockEnvironment(); oidcEnvironment.setActiveProfiles("oidc"); - PortalUserSessionFilter oidcFilter = new PortalUserSessionFilter(oidcEnvironment); + UserInfoHolder userInfoHolder = mock(UserInfoHolder.class); + PortalUserSessionFilter oidcFilter = + new PortalUserSessionFilter(oidcEnvironment, userInfoHolder); MockHttpServletRequest request = new MockHttpServletRequest("GET", OPEN_API_URI); request.setCookies(new Cookie("SESSION", "expired")); @@ -252,4 +267,32 @@ public void openApiRequestWithoutLoginOrToken_shouldReturn401() throws Exception mockMvc.perform(get(OPEN_API_URI)) .andExpect(status().isUnauthorized()); } + + @Test + public void openApiPortalRequestWithOperatorParam_shouldPopulateUserIdentityContext() + throws Exception { + MockEnvironment environment = new MockEnvironment(); + PortalUserSessionFilter filter = new PortalUserSessionFilter(environment, userInfoHolder); + + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + "apollo", "password", AuthorityUtils.createAuthorityList("ROLE_user")); + SecurityContextHolder.getContext().setAuthentication(authentication); + + UserInfo portalUser = new UserInfo(); + portalUser.setUserId("apollo"); + when(userInfoHolder.getUser()).thenReturn(portalUser); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", OPEN_API_URI); + request.addParameter("operator", "keep-current-user"); + MockHttpServletResponse response = new MockHttpServletResponse(); + AtomicBoolean chainCalled = new AtomicBoolean(false); + + filter.doFilter(request, response, (req, resp) -> { + chainCalled.set(true); + org.junit.Assert.assertEquals(portalUser, UserIdentityContextHolder.getOperator()); + }); + + org.junit.Assert.assertTrue(chainCalled.get()); + org.junit.Assert.assertNull(UserIdentityContextHolder.getOperator()); + } } diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/AppServiceTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/AppServiceTest.java index 8ddb987cff8..31753cb0eaa 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/AppServiceTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/service/AppServiceTest.java @@ -24,6 +24,7 @@ import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.portal.api.AdminServiceAPI; import com.ctrip.framework.apollo.portal.component.PortalSettings; +import com.ctrip.framework.apollo.portal.component.UserIdentityContextHolder; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.repository.AppRepository; import com.ctrip.framework.apollo.portal.spi.UserInfoHolder; @@ -32,6 +33,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; @@ -89,7 +91,12 @@ void beforeEach() { apolloAuditLogApi); UserInfo userInfo = new UserInfo(); userInfo.setUserId(OPERATOR_USER_ID); - Mockito.when(userInfoHolder.getUser()).thenReturn(userInfo); + UserIdentityContextHolder.setOperator(userInfo); + } + + @AfterEach + void afterEach() { + UserIdentityContextHolder.clear(); } @Test @@ -132,7 +139,6 @@ void createAppAndAddRolePermission() { appService.createAppAndAddRolePermission(app, admins); Mockito.verify(appRepository, Mockito.times(1)).findByAppId(Mockito.eq(appId)); Mockito.verify(userService, Mockito.times(1)).findByUserId(Mockito.eq(userId)); - Mockito.verify(userInfoHolder, Mockito.times(2)).getUser(); Mockito.verify(appRepository, Mockito.times(1)).save(Mockito.eq(app)); Mockito.verify(appNamespaceService, Mockito.times(1)) .createDefaultAppNamespace(Mockito.eq(appId)); @@ -159,7 +165,6 @@ void testDeleteAppInLocal() { App deletedApp = appService.deleteAppInLocal(appId); Mockito.verify(appRepository, Mockito.times(1)).deleteApp(Mockito.eq(appId), Mockito.eq(OPERATOR_USER_ID)); - Mockito.verify(userInfoHolder, Mockito.times(1)).getUser(); Mockito.verify(apolloAuditLogApi, Mockito.times(1)).appendDataInfluences( Mockito.eq(Collections.singletonList(deletedApp)), Mockito.eq(App.class)); Mockito.verify(appNamespaceService, Mockito.times(1)).batchDeleteByAppId(Mockito.eq(appId),