/* * Copyright 2000-2018 JetBrains s.r.o. * * 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 jetbrains.buildServer.server.rest.data; import com.intellij.openapi.diagnostic.Logger; import java.util.*; import java.util.stream.Collectors; import jetbrains.buildServer.BuildProject; import jetbrains.buildServer.ServiceLocator; import jetbrains.buildServer.server.rest.APIController; import jetbrains.buildServer.server.rest.errors.AuthorizationFailedException; import jetbrains.buildServer.server.rest.errors.NotFoundException; import jetbrains.buildServer.server.rest.model.project.Project; import jetbrains.buildServer.server.rest.model.project.PropEntityProjectFeature; import jetbrains.buildServer.server.rest.util.BuildTypeOrTemplate; import jetbrains.buildServer.serverSide.*; import jetbrains.buildServer.serverSide.agentPools.AgentPool; import jetbrains.buildServer.serverSide.auth.AccessDeniedException; import jetbrains.buildServer.serverSide.auth.Permission; import jetbrains.buildServer.users.SUser; import jetbrains.buildServer.users.impl.UserEx; import jetbrains.buildServer.util.StringUtil; import jetbrains.buildServer.vcs.SVcsRoot; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * @author Yegor.Yarko * Date: 23.03.13 */ public class ProjectFinder extends AbstractFinder { private static final Logger LOG = Logger.getInstance(ProjectFinder.class.getName()); public static final String DIMENSION_ID = AbstractFinder.DIMENSION_ID; public static final String DIMENSION_INTERNAL_ID = "internalId"; public static final String DIMENSION_UUID = "uuid"; public static final String DIMENSION_PROJECT = "project"; public static final String DIMENSION_PARENT_PROJECT = "parentProject"; private static final String DIMENSION_AFFECTED_PROJECT = "affectedProject"; public static final String DIMENSION_NAME = "name"; public static final String DIMENSION_ARCHIVED = "archived"; public static final String DIMENSION_READ_ONLY_UI = "readOnlyUI"; protected static final String DIMENSION_PARAMETER = "parameter"; protected static final String DIMENSION_SELECTED = "selectedByUser"; public static final String BUILD = "build"; public static final String BUILD_TYPE = "buildType"; public static final String DEFAULT_TEMPLATE = "defaultTemplate"; public static final String VCS_ROOT = "vcsRoot"; public static final String AGENT_POOL = "pool"; public static final String FEATURE = "projectFeature"; public static final String USER_PERMISSION = "userPermission"; @NotNull private final ProjectManager myProjectManager; private final PermissionChecker myPermissionChecker; @NotNull private final ServiceLocator myServiceLocator; public ProjectFinder(@NotNull final ProjectManager projectManager, final PermissionChecker permissionChecker, @NotNull final ServiceLocator serviceLocator){ super(DIMENSION_ID, DIMENSION_INTERNAL_ID, DIMENSION_UUID, DIMENSION_PROJECT, DIMENSION_AFFECTED_PROJECT, DIMENSION_NAME, DIMENSION_ARCHIVED, BUILD, BUILD_TYPE, DEFAULT_TEMPLATE, VCS_ROOT, FEATURE, AGENT_POOL, Locator.LOCATOR_SINGLE_VALUE_UNUSED_NAME); setHiddenDimensions(DIMENSION_PARAMETER, DIMENSION_SELECTED, DIMENSION_READ_ONLY_UI, USER_PERMISSION, DIMENSION_LOOKUP_LIMIT, DIMENSION_PARENT_PROJECT //compatibility mode for versions <9.1 ); myProjectManager = projectManager; myPermissionChecker = permissionChecker; myServiceLocator = serviceLocator; } @NotNull @Override public String getItemLocator(@NotNull final SProject project) { return ProjectFinder.getLocator(project); } public static String getLocator(final BuildProject project) { return Locator.getStringLocator(DIMENSION_ID, project.getExternalId()); } @Nullable @Override public SProject findSingleItem(@NotNull final Locator locator) { if (locator.isSingleValue()) { // no dimensions found, assume it's a name or internal id or external id SProject project = null; @SuppressWarnings("ConstantConditions") @NotNull final String singleValue = locator.getSingleValue(); project = myProjectManager.findProjectByExternalId(singleValue); if (project != null) { return project; } final List projectsByName = findProjectsByName(null, singleValue, true); if (projectsByName.size() == 1) { return projectsByName.get(0); } project = myProjectManager.findProjectById(singleValue); if (project != null) { return project; } throw new NotFoundException("No project found by name or internal/external id '" + singleValue + "'."); } String id = locator.getSingleDimensionValue(DIMENSION_ID); if (id != null) { SProject project = myProjectManager.findProjectByExternalId(id); if (project == null) { if (TeamCityProperties.getBoolean(APIController.REST_COMPATIBILITY_ALLOW_EXTERNAL_ID_AS_INTERNAL)) { project = myProjectManager.findProjectById(id); if (project == null) { throw new NotFoundException("No project found by locator '" + locator.getStringRepresentation() + "' in compatibility mode. Project cannot be found by external or internal id '" + id + "'."); } } else { throw new NotFoundException("No project found by locator '" + locator.getStringRepresentation() + "'. Project cannot be found by external id '" + id + "'."); } } return project; } String internalId = locator.getSingleDimensionValue(DIMENSION_INTERNAL_ID); if (internalId != null) { SProject project = myProjectManager.findProjectById(internalId); if (project == null) { throw new NotFoundException("No project found by locator '" + locator.getStringRepresentation() + "'. Project cannot be found by internal id '" + internalId + "'."); } return project; } String uuid = locator.getSingleDimensionValue(DIMENSION_UUID); if (!StringUtil.isEmpty(uuid)) { SProject project = myProjectManager.findProjectByConfigId(uuid); if (project == null) { //protecting against brute force uuid guessing try { Thread.sleep(1000); } catch (InterruptedException e) { //ignore } throw new NotFoundException("No project found by locator '" + locator.getStringRepresentation() + "'. Project cannot be found by uuid '" + uuid + "'."); } return project; } String buildLocator = locator.getSingleDimensionValue(BUILD); if (!StringUtil.isEmpty(buildLocator)) { BuildPromotion build = myServiceLocator.getSingletonService(BuildPromotionFinder.class).getItem(buildLocator); SBuildType buildType = build.getBuildType(); if (buildType != null) { return buildType.getProject(); } } String buildTypeLocator = locator.getSingleDimensionValue(BUILD_TYPE); if (!StringUtil.isEmpty(buildTypeLocator)) { BuildTypeOrTemplate buildType = myServiceLocator.getSingletonService(BuildTypeFinder.class).getItem(buildTypeLocator); return buildType.getProject(); } String vcsRootLocator = locator.getSingleDimensionValue(VCS_ROOT); if (!StringUtil.isEmpty(vcsRootLocator)) { SVcsRoot vcsRoot = myServiceLocator.getSingletonService(VcsRootFinder.class).getItem(vcsRootLocator); return vcsRoot.getProject(); } return null; } @Nullable private SProject getParentProject(final @NotNull Locator locator) { String parentProjectLocator = locator.getSingleDimensionValue(DIMENSION_PARENT_PROJECT); //compatibility mode for versions <9.1 if (parentProjectLocator == null) parentProjectLocator = locator.getSingleDimensionValue(DIMENSION_AFFECTED_PROJECT); return parentProjectLocator == null ? null : getItem(parentProjectLocator); } @NotNull @Override public ItemFilter getFilter(@NotNull final Locator locator) { final MultiCheckerFilter result = new MultiCheckerFilter(); final String name = locator.getSingleDimensionValue(DIMENSION_NAME); if (name != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SProject item) { return name.equals(item.getName()); } }); } final Boolean archived = locator.getSingleDimensionValueAsBoolean(DIMENSION_ARCHIVED); if (archived != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SProject item) { return FilterUtil.isIncludedByBooleanFilter(archived, item.isArchived()); } }); } final Boolean readOnlyUI = locator.getSingleDimensionValueAsBoolean(DIMENSION_READ_ONLY_UI); if (readOnlyUI != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SProject item) { return FilterUtil.isIncludedByBooleanFilter(readOnlyUI, item.isReadOnly()); } }); } final String parameterDimension = locator.getSingleDimensionValue(DIMENSION_PARAMETER); if (parameterDimension != null) { final ParameterCondition parameterCondition = ParameterCondition.create(parameterDimension); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SProject item) { final boolean canView = !Project.shouldRestrictSettingsViewing(item, myPermissionChecker); if (!canView) { LOG.debug("While filtering projects by " + DIMENSION_PARAMETER + " user does not have enough permissions to see settings. Excluding project: " + item.describe(false)); return false; } return parameterCondition.matches(item); } }); } if (locator.isUnused(DIMENSION_PROJECT)) { final String directParentLocator = locator.getSingleDimensionValue(DIMENSION_PROJECT); if (directParentLocator != null) { final SProject directParent = getItem(directParentLocator); result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SProject item) { return directParent.getProjectId().equals(item.getParentProjectId()); } }); } } if (locator.isUnused(DIMENSION_AFFECTED_PROJECT)) { final SProject parentProject = getParentProject(locator); if (parentProject != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SProject item) { return isSameOrParent(parentProject, item); } }); } } final String featureDimension = locator.getSingleDimensionValue(FEATURE); if (featureDimension != null) { result.add(new FilterConditionChecker() { public boolean isIncluded(@NotNull final SProject item) { final boolean canView = !Project.shouldRestrictSettingsViewing(item, myPermissionChecker); if (!canView) { LOG.debug("While filtering projects by " + DIMENSION_PARAMETER + " user does not have enough permissions to see settings. Excluding project: " + item.describe(false)); return false; } return new PropEntityProjectFeature.ProjectFeatureFinder(item).getItems(featureDimension).myEntries.size() > 0; } }); } final String defaultTemplateDimension = locator.getSingleDimensionValue(DEFAULT_TEMPLATE); if (defaultTemplateDimension != null) { Set defaultTemplateIds = myServiceLocator.getSingletonService(BuildTypeFinder.class). getItems(defaultTemplateDimension).myEntries.stream().map(bt -> bt.getInternalId()).collect(Collectors.toSet()); result.add(item -> { final boolean canView = !Project.shouldRestrictSettingsViewing(item, myPermissionChecker); if (!canView) { LOG.debug("While filtering projects by " + DIMENSION_PARAMETER + " user does not have enough permissions to see settings. Excluding project: " + item.describe(false)); return false; } String defaultTemplateId = item.getDefaultTemplateId(); return defaultTemplateId != null && defaultTemplateIds.contains(defaultTemplateId); }); } final String userPermissionDimension = locator.getSingleDimensionValue(USER_PERMISSION); if (userPermissionDimension != null) { result.add(new PermissionCheck().matches(userPermissionDimension)); } final List poolDimensions = locator.getDimensionValue(AGENT_POOL); if (!poolDimensions.isEmpty()) { AgentPoolFinder agentPoolFinder = myServiceLocator.getSingletonService(AgentPoolFinder.class); // streams API alternative //Optional> filterProjectInternalIds = poolDimensions.stream(). // map(poolLocator -> agentPoolFinder.getItems(poolLocator).myEntries.stream().flatMap(pool -> pool.getProjectIds().stream()).collect(Collectors.toSet())). // reduce((projectIds, projectIds2) -> projectIds.stream().filter(projectIds2::contains).collect(Collectors.toSet())); //if (filterProjectInternalIds.isPresent()){ // result.add(item -> filterProjectInternalIds.get().contains(item.getProjectId())); //} for (String poolDimension : poolDimensions) { List pools = agentPoolFinder.getItems(poolDimension).myEntries; Set filterProjectInternalIds = new HashSet<>(); for (AgentPool pool : pools) { filterProjectInternalIds.addAll(pool.getProjectIds()); } result.add(item -> filterProjectInternalIds.contains(item.getProjectId())); } } return result; } @NotNull @Override public ItemHolder getPrefilteredItems(@NotNull final Locator locator) { //this should be the first one as the order returned here is important! final String selectedForUser = locator.getSingleDimensionValue(DIMENSION_SELECTED); if (selectedForUser != null) { Locator selectedByUserLocator = new Locator(selectedForUser, "user", "mode"); String userLocator = selectedByUserLocator.getSingleDimensionValue("user"); String modeLocator = selectedByUserLocator.getSingleDimensionValue("mode"); if (userLocator == null && modeLocator == null) { //assume it's user locator - the only supported way before 2017.1 userLocator = selectedForUser; } else { selectedByUserLocator.checkLocatorFullyProcessed(); } final SUser user = myServiceLocator.getSingletonService(UserFinder.class).getItem(userLocator); return getItemHolder(getSelectedProjects(user, getSelectedByUserMode(modeLocator, SelectedByUserMode.SELECTED_AND_UNKNOWN))); } final SProject parentProject; try { parentProject = getParentProject(locator); } catch (AccessDeniedException e) { //workaround for https://youtrack.jetbrains.com/issue/TW-46290 // note: finding affectedProject under system is a bad approach as that can be used to probe server data without due permissions, so support only some hardcoded cases here String affectedProjectDimension = locator.lookupSingleDimensionValue(DIMENSION_AFFECTED_PROJECT); if (getRootProject().getExternalId().equals(affectedProjectDimension) || getLocator(getRootProject()).equals(affectedProjectDimension)) { // it was a request with affectedProject==_Root, but user got access denied on finding, returning empty collection instead of 403/Forbidden return getItemHolder(Collections.emptyList()); } throw e; //throw original error } final String name = locator.getSingleDimensionValue(DIMENSION_NAME); if (name != null) { if (parentProject != null) { return getItemHolder(findProjectsByName(parentProject, name, true)); } String directParent = locator.getSingleDimensionValue(DIMENSION_PROJECT); if (directParent != null){ return getItemHolder(findProjectsByName(getItem(directParent), name, false)); } } final String directParent = locator.getSingleDimensionValue(DIMENSION_PROJECT); if (directParent != null) { return getItemHolder(getItem(directParent).getOwnProjects()); } if (parentProject != null) { return getItemHolder(parentProject.getProjects()); } return getItemHolder(myProjectManager.getProjects()); } public static enum SelectedByUserMode { SELECTED, //those which are selected on Overview, in the order these were configured on Overview (this order can be different from the current hierarchy order) SELECTED_WITH_ORDER, //experimental! those which are selected on Overview, in hierarchy-defined order and selected order for siblings within hierarchy SELECTED_AND_UNKNOWN, //as in SELECTED plus those which has mo mark on selection or hiding, in Overview-configured order ALL_WITH_ORDER; //all the projects which user can see in the order defined on Overview, but abiding the hierarchy depth-first traversing (i.e. as should be shown in projects pop-up) } @NotNull public static SelectedByUserMode getSelectedByUserMode(@Nullable final String textName, @NotNull final SelectedByUserMode defaultValue) { return TypedFinderBuilder.getEnumValue(textName != null ? textName : defaultValue.name(), SelectedByUserMode.class); } @NotNull public Collection getSelectedProjects(@NotNull final SUser user, @NotNull final SelectedByUserMode mode) { switch (mode) { case SELECTED: //TeamCity API issue: cast return myProjectManager.findProjects(((UserEx)user).getProjectVisibilityHolder().getKnownVisibleProjects()); case SELECTED_WITH_ORDER: ArrayList result = new ArrayList<>(myProjectManager.findProjects(((UserEx)user).getProjectVisibilityHolder().getKnownVisibleProjects())); Collections.sort(result, new ProjectComparator()); return result; case ALL_WITH_ORDER: return ((UserEx)user).getProjectVisibilityHolder().getAllProjectsOrdered(); default: case SELECTED_AND_UNKNOWN: //this is pre-2017.1 behavior return myProjectManager.findProjects(user.getVisibleProjects()); } } @NotNull public SProject getRootProject() { return myProjectManager.getRootProject(); } /** * Finds projects with the given name under the project specified * @param parentProject Project under which to search. If 'null' - process all projects including root one. * @param name * @param recursive * @return */ @NotNull private List findProjectsByName(@Nullable SProject parentProject, @NotNull final String name, final boolean recursive) { final ArrayList result = new ArrayList(); if (parentProject == null) { parentProject = getRootProject(); if (name.equals(parentProject.getName())) { //process root project as well result.add(parentProject); } } final List projects = recursive ? parentProject.getProjects() : parentProject.getOwnProjects(); for (SProject project : projects) { if (name.equals(project.getName())){ result.add(project); } } return result; } @Nullable public BuildProject findProjectByInternalId(final String projectInternalId) { return myProjectManager.findProjectById(projectInternalId); } @NotNull public static SProject getProjectByInternalId(@NotNull final String projectInternalId, @NotNull final ProjectManager projectManager) { final SProject project = projectManager.findProjectById(projectInternalId); if (project == null) { throw new NotFoundException("No project found by internal id '" + projectInternalId + "'."); } return project; } public static boolean isSameOrParent(@NotNull final BuildProject parent, @NotNull final BuildProject project) { return isSameOrParent(Collections.singleton(parent), project); } /** * Calculates if the project is one of or an (indirect) child of the parent candidates passed */ public static boolean isSameOrParent(@NotNull final Collection parents, @NotNull final BuildProject project) { Set projectIds = parents.stream().map(p -> p.getProjectId()).collect(Collectors.toSet()); BuildProject currentProject = project; while(currentProject != null) { if (projectIds.contains(currentProject.getProjectId())) return true; currentProject = currentProject.getParentProject(); } return false; } @NotNull public SProject getItem(@Nullable final String locatorText, final boolean checkViewSettingsPermission) { final SProject result = getItem(locatorText); if (checkViewSettingsPermission) { check(result, myPermissionChecker); } return result; } @NotNull public PagedSearchResult getItems(final @Nullable SProject parentProject, final @Nullable String projectLocator) { String actualLocator = projectLocator; if (parentProject != null && (projectLocator == null || !(new Locator(projectLocator)).isSingleValue())) { actualLocator = Locator.setDimensionIfNotPresent(projectLocator, DIMENSION_PROJECT, ProjectFinder.getLocator(parentProject)); } return getItems(actualLocator); } public static void check(@NotNull SProject project, @NotNull final PermissionChecker permissionChecker) { if (Project.shouldRestrictSettingsViewing(project, permissionChecker)) { throw new AuthorizationFailedException( "User does not have '" + Permission.VIEW_BUILD_CONFIGURATION_SETTINGS.getName() + "' permission in project " + project.describe(false)); } } //See also jetbrains.buildServer.server.rest.data.UserFinder.PermissionCheck private class PermissionCheck { private final TypedFinderBuilder.Dimension> USER = new TypedFinderBuilder.Dimension<>("user"); private final TypedFinderBuilder.Dimension PERMISSION = new TypedFinderBuilder.Dimension<>("permission"); private final Finder myFinder; PermissionCheck() { TypedFinderBuilder builder = new TypedFinderBuilder(); builder.dimensionUsers(USER, myServiceLocator).description("user to check permission for, should be present"); builder.dimensionEnum(PERMISSION, Permission.class).description("permission to check, should be present"); builder.filter(locator -> locator.lookupSingleDimensionValue(PERMISSION.name) != null && locator.lookupSingleDimensionValue(USER.name) != null, dimensions -> new PermissionFilter(dimensions)); myFinder = builder.build(); } ItemFilter matches(@NotNull String permissionCheckLocator) { return myFinder.getFilter(permissionCheckLocator); } private class PermissionFilter implements ItemFilter { private final Permission myPermission; private final List myUsers; PermissionFilter(final TypedFinderBuilder.DimensionObjects dimensions) { //noinspection ConstantConditions - is checked in a filter condition earlier myPermission = dimensions.get(PERMISSION).get(0); //noinspection ConstantConditions - is checked in a filter condition earlier myUsers = dimensions.get(USER).get(0); } @Override public boolean shouldStop(@NotNull final SProject item) { return false; } @Override public boolean isIncluded(@NotNull final SProject item) { return PermissionChecker.anyOfUsersHavePermissionForAnyOfProjects(myUsers, myPermission, Collections.singletonList(item)); } } } }