/* * 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 com.intellij.util.Function; import java.util.*; import jetbrains.buildServer.BuildProject; import jetbrains.buildServer.ServiceLocator; import jetbrains.buildServer.groups.SUserGroup; import jetbrains.buildServer.parameters.ParametersProvider; import jetbrains.buildServer.parameters.impl.MapParametersProviderImpl; import jetbrains.buildServer.server.rest.errors.AuthorizationFailedException; import jetbrains.buildServer.server.rest.errors.BadRequestException; import jetbrains.buildServer.server.rest.errors.LocatorProcessException; import jetbrains.buildServer.server.rest.errors.NotFoundException; import jetbrains.buildServer.serverSide.SProject; import jetbrains.buildServer.serverSide.TeamCityProperties; import jetbrains.buildServer.serverSide.auth.*; import jetbrains.buildServer.users.PropertyKey; import jetbrains.buildServer.users.SUser; import jetbrains.buildServer.users.User; import jetbrains.buildServer.users.UserModel; import jetbrains.buildServer.users.impl.UserImpl; import jetbrains.buildServer.util.CollectionsUtil; import jetbrains.buildServer.util.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import static jetbrains.buildServer.server.rest.data.FinderDataBinding.getItemHolder; import static jetbrains.buildServer.server.rest.data.TypedFinderBuilder.Dimension; /** * @author Yegor.Yarko * Date: 23.03.13 */ public class UserFinder extends DelegatingFinder { private static final Logger LOG = Logger.getInstance(jetbrains.buildServer.serverSide.impl.audit.finders.UserFinder.class.getName()); public static final String REST_CHECK_ADDITIONAL_PERMISSIONS_ON_USERS_AND_GROUPS = "rest.request.checkAdditionalPermissionsForUsersAndGroups"; private static final Dimension ID = new Dimension<>("id"); private static final Dimension USERNAME = new Dimension<>("username"); private static final Dimension GROUP = new Dimension<>("group"); private static final Dimension AFFECTED_GROUP = new Dimension<>("affectedGroup"); private static final Dimension PROPERTY = new Dimension<>("property"); private static final Dimension EMAIL = new Dimension<>("email"); private static final Dimension NAME = new Dimension<>("name"); private static final Dimension HAS_PASSWORD = new Dimension<>("hasPassword"); private static final Dimension PASSWORD = new Dimension<>("password"); private static final Dimension LAST_LOGIN_TIME = new Dimension<>("lastLogin"); private static final Dimension ROLE = new Dimension<>("role"); private static final Dimension> PERMISSION = new Dimension<>("permission"); @NotNull private final UserModel myUserModel; @NotNull private final UserGroupFinder myGroupFinder; @NotNull private final ProjectFinder myProjectFinder; @NotNull private final TimeCondition myTimeCondition; @NotNull private final RolesManager myRolesManager; @NotNull private final PermissionChecker myPermissionChecker; @NotNull private final SecurityContext mySecurityContext; @NotNull private final ServiceLocator myServiceLocator; public UserFinder(@NotNull final UserModel userModel, @NotNull final UserGroupFinder groupFinder, @NotNull final ProjectFinder projectFinder, @NotNull final TimeCondition timeCondition, @NotNull final RolesManager rolesManager, @NotNull final PermissionChecker permissionChecker, @NotNull final SecurityContext securityContext, @NotNull final ServiceLocator serviceLocator) { myUserModel = userModel; myGroupFinder = groupFinder; myProjectFinder = projectFinder; myTimeCondition = timeCondition; myRolesManager = rolesManager; myPermissionChecker = permissionChecker; mySecurityContext = securityContext; myServiceLocator = serviceLocator; setDelegate(new UserFinderBuilder().build()); } public void checkViewAllUsersPermissionEnforced() throws AuthorizationFailedException { if (TeamCityProperties.getBooleanOrTrue(REST_CHECK_ADDITIONAL_PERMISSIONS_ON_USERS_AND_GROUPS)) { checkViewAllUsersPermission(); } } public void checkViewAllUsersPermission() throws AuthorizationFailedException { myPermissionChecker.checkGlobalPermissionAnyOf(new Permission[]{Permission.VIEW_USER_PROFILE, Permission.CHANGE_USER}); } public void checkViewUserPermission(final @Nullable SUser user) throws AuthorizationFailedException { if (user != null) { final jetbrains.buildServer.users.User currentUser = getCurrentUser(); if (currentUser != null && currentUser.getId() == user.getId()) { return; } } try { checkViewAllUsersPermission(); } catch (AuthorizationFailedException e) { throw new AuthorizationFailedException("No permission to reference users: " + e.getMessage(), e); } } @Nullable public SUser getCurrentUser() { //also related API: SessionUser.getUser(request) final User associatedUser = mySecurityContext.getAuthorityHolder().getAssociatedUser(); if (associatedUser == null) { return null; } if (SUser.class.isAssignableFrom(associatedUser.getClass())) { return (SUser)associatedUser; } return myUserModel.findUserAccount(null, associatedUser.getUsername()); } @NotNull private static ParametersProvider getUserPropertiesProvider(@NotNull final SUser item) { Map properties = item.getProperties(); HashMap result = new HashMap<>(); for (Map.Entry entry : properties.entrySet()) { result.put(entry.getKey().getKey(), entry.getValue()); } return new MapParametersProviderImpl(result); } @NotNull public static List convert(Collection users) { ArrayList result = new ArrayList<>(users.size()); for (User user : users) { if (SUser.class.isAssignableFrom(user.getClass())) { result.add((SUser)user); } else { LOG.info("Got User which is not SUser (skipping): " + user.describe(true)); } } return result; } public static String getLocatorById(@NotNull final Long id) { return Locator.getStringLocator(ID.name, String.valueOf(id)); } public static String getLocatorByUsername(@NotNull final String username) { return Locator.getStringLocator(USERNAME.name, username); } public static String getLocatorByGroup(@NotNull final SUserGroup userGroup) { return Locator.getStringLocator(GROUP.name, UserGroupFinder.getLocator(userGroup)); } @NotNull public static String getLocator(@NotNull final User user) { return getLocatorById(user.getId()); } @NotNull public SUser getItem(@Nullable final String locatorText, boolean checkViewPermission) { if (checkViewPermission) { SUser user = null; try { user = super.getItem(locatorText); return user; } finally { //should check in case of NotFoundException as well. this will mask original exception in cae of no permissions, but that is exactly what is necessary if (TeamCityProperties.getBooleanOrTrue(REST_CHECK_ADDITIONAL_PERMISSIONS_ON_USERS_AND_GROUPS)) { //related to http://youtrack.jetbrains.net/issue/TW-20071 and other cases checkViewUserPermission(user); } } } return super.getItem(locatorText); } static class RoleEntryDatas { private final Set anyScopeRoles = new HashSet<>(); private final Set anyRoleScopes = new HashSet<>(); private final Map> roleScopes = new HashMap<>(); @NotNull private final RolesManager myRolesManager; @NotNull private final ProjectFinder myProjectFinder; @NotNull private final PermissionChecker myPermissionChecker; @NotNull private final String myMethod; public RoleEntryDatas(@NotNull final String roleLocatorText, @NotNull final RolesManager rolesManager, @NotNull final ProjectFinder projectFinder, @NotNull final PermissionChecker permissionChecker) { Locator roleLocator = new Locator(roleLocatorText, "item", "method", Locator.LOCATOR_SINGLE_VALUE_UNUSED_NAME); //consider using generic "item" support from FinderImpl List roleAssignmentsLocatorTexts; String method = null; if (roleLocator.isSingleValue()) { roleAssignmentsLocatorTexts = Collections.singletonList(roleLocator.getSingleValue()); } else { roleAssignmentsLocatorTexts = roleLocator.getDimensionValue("item"); method = roleLocator.getSingleDimensionValue("method"); } if (method == null && roleAssignmentsLocatorTexts.isEmpty()) { roleAssignmentsLocatorTexts = Collections.singletonList(roleLocatorText); } else { roleLocator.checkLocatorFullyProcessed(); } if (method == null) method = "effective"; if (!"effective".equals(method) && !"byPermission".equals(method)) { //at some point can add locator dimensions to search direct roles or selectively considering projects, roles, groups nesting throw new BadRequestException("Unknown '" + "method" + "' role dimension value '" + method + "'. Supported are: " + "effective" + ", " + "byPermission"); } //parsed, not initialize myMethod = method; myRolesManager = rolesManager; myProjectFinder = projectFinder; myPermissionChecker = permissionChecker; for (String value : roleAssignmentsLocatorTexts) { Locator locator = new Locator(value, "scope", "role"); String scope = locator.getSingleDimensionValue("scope"); String role = locator.getSingleDimensionValue("role"); if (scope == null && role == null) { throw new LocatorProcessException("Invalid role locator '" + roleLocator + ": either 'scope' or 'role' dimension should be present"); } locator.checkLocatorFullyProcessed(); add(getScope(scope), getRole(role)); } } boolean matches(@NotNull final SUser item) { if ("effective".equals(myMethod)) { return containsAllRolesEffectively(item); } //else if ("byPermission".equals(myMethod)) return containsAllRolesByPermissions(item); } /** * Considers roles, projects and user groups nesting */ boolean containsAllRolesEffectively(@NotNull final RolesHolder mainHolder) { Set anyScopeRoles = new HashSet<>(this.anyScopeRoles); Set anyRoleScopes = new HashSet<>(this.anyRoleScopes); Map> roleScopes = new HashMap<>(this.roleScopes.size()); for (Map.Entry> entry : this.roleScopes.entrySet()) { roleScopes.put(entry.getKey(), new HashSet<>(entry.getValue())); } for (RolesHolder holder : CollectionsUtil.join(Collections.singletonList(mainHolder), mainHolder.getAllParentHolders())) { for (RoleEntry mainRole : holder.getRoles()) { anyRoleScopes.remove(mainRole.getScope()); for (Role role : CollectionsUtil.join(Collections.singletonList(mainRole.getRole()), Arrays.asList(mainRole.getRole().getIncludedRoles()))) { Set scopes = roleScopes.get(role); if (scopes != null) { if (mainRole.getScope().isGlobal()) { roleScopes.remove(role); } else { scopes.remove(mainRole.getScope()); BuildProject project = myProjectFinder.findProjectByInternalId(mainRole.getScope().getProjectId()); if (project != null) { for (BuildProject subProject : project.getProjects()) { scopes.remove(RoleScope.projectScope(subProject.getProjectId())); } } } if (scopes.isEmpty()) roleScopes.remove(role); } anyScopeRoles.remove(role); } if (anyScopeRoles.isEmpty() && anyRoleScopes.isEmpty() && roleScopes.isEmpty()) return true; } } return false; } private Permissions globalPermissions = null; private Map projectsPermissions = null; /** * does matching by effective permissions */ boolean containsAllRolesByPermissions(@NotNull final AuthorityHolder mainHolder) { if (globalPermissions == null) { initPermissions(); } Permissions holderGlobalPermissions = mainHolder.getGlobalPermissions(); Map holderProjectsPermissions = mainHolder.getProjectsPermissions(); if (!holderGlobalPermissions.containsAll(globalPermissions)) return false; for (Map.Entry requiredPermissions : projectsPermissions.entrySet()) { Permissions actualPermissions = holderProjectsPermissions.get(requiredPermissions.getKey()); if (actualPermissions == null) { return holderGlobalPermissions.containsAll(requiredPermissions.getValue()); } if (!getCombined(actualPermissions, holderGlobalPermissions).containsAll(requiredPermissions.getValue())) return false; } return true; } private void initPermissions() { if (!anyScopeRoles.isEmpty() || !anyRoleScopes.isEmpty() || roleScopes.isEmpty()) { throw new BadRequestException("When matching roles by permissions, both scope and role should be specified."); } globalPermissions = new Permissions(); projectsPermissions = new HashMap<>(); for (Map.Entry> entry : roleScopes.entrySet()) { for (RoleScope scope : entry.getValue()) { if (scope.isGlobal()) { globalPermissions = getCombined(entry.getKey().getPermissions(), this.globalPermissions); } else { projectsPermissions.put(scope.getProjectId(), getCombined(entry.getKey().getPermissions(), projectsPermissions.get(scope.getProjectId()))); } } } } private static Permissions getCombined(@NotNull final Permissions permissions1, @Nullable final Permissions permissions2) { BitSet mask = new BitSet(); mask.or(permissions1.getMask()); if (permissions2 != null) mask.or(permissions2.getMask()); return new Permissions(mask); } private void add(@Nullable final RoleScope scope, @Nullable final Role role) { if (scope == null && role == null) { throw new BadRequestException("Either 'scope' or 'role' should be defined"); } if (scope == null) { anyScopeRoles.add(role); } else if (role == null) { anyRoleScopes.add(scope); } else { Set scopes = roleScopes.get(role); if (scopes == null) { scopes = new HashSet<>(); roleScopes.put(role, scopes); } scopes.add(scope); } } // See also jetbrains.buildServer.server.rest.model.user.RoleAssignment.getScope() @Nullable private RoleScope getScope(@Nullable String scopeLocator) { if (scopeLocator == null) return null; Locator locator = new Locator(scopeLocator, "project", "p", Locator.LOCATOR_SINGLE_VALUE_UNUSED_NAME); if (locator.isSingleValue()) { if ("global".equalsIgnoreCase(locator.getSingleValue()) || "g".equalsIgnoreCase(locator.getSingleValue())) { //g is for compatibility return RoleScope.globalScope(); } throw new BadRequestException("Invalid scope specification '" + scopeLocator + "'. Should be 'global' or project:"); } String projectLocator = locator.getSingleDimensionValue("project"); if (projectLocator == null) { projectLocator = locator.getSingleDimensionValue("p"); //compatibility } if (projectLocator == null) { throw new BadRequestException("Invalid scope locator '" + scopeLocator + "'. Should be 'global' or project:"); } locator.checkLocatorFullyProcessed(); SProject project = myProjectFinder.getItem(projectLocator); return RoleScope.projectScope(project.getProjectId()); } @Nullable private Role getRole(@Nullable String roleLocator) { if (roleLocator == null) return null; Locator locator = new Locator(roleLocator, "id", Locator.LOCATOR_SINGLE_VALUE_UNUSED_NAME); String roleId = null; if (locator.isSingleValue()) { roleId = locator.getSingleValue(); } else { String id = locator.getSingleDimensionValue("id"); if (id != null) { roleId = id; } } if (roleId == null) { throw new BadRequestException(addRoleIdsIfApplicable("Invalid role locator '" + roleLocator + "'. Should contain id of one of the roles defined on the server.")); } locator.checkLocatorFullyProcessed(); Role role = myRolesManager.findRoleById(roleId); if (role == null) { role = myRolesManager.findRoleById(roleId.toUpperCase()); //support for any case if (role == null) { throw new NotFoundException(addRoleIdsIfApplicable("Cannot find role by id '" + roleId + "'.")); } } return role; } private String addRoleIdsIfApplicable(final String initialMessage) { if (myPermissionChecker.isPermissionGranted(Permission.VIEW_USER_PROFILE, null)) { return initialMessage + " Available role ids: " + StringUtil.join(myRolesManager.getAvailableRoles(), new Function() { @Override public String fun(final Role role) { return role.getId(); } }, ", ") + "."; } return initialMessage; } } private class UserFinderBuilder extends TypedFinderBuilder { UserFinderBuilder() { name("UserFinder"); singleDimension(dimension -> { //"current" is a reserved value for single dimension - current user. // before 2018.1 the precedence was different: if there was a user with username "current" it was served. However, the behavior was poorly predictable that way. if ("current".equals(dimension)) { // support for predefined "current" keyword to get current user final SUser currentUser = getCurrentUser(); if (currentUser == null) { throw new NotFoundException("No current user."); } else { return Collections.singletonList(currentUser); } } // no dimensions found and it's not reserved current" -> assume it's username SUser user = myUserModel.findUserAccount(null, dimension); if (user == null) { throw new NotFoundException("No user can be found by username '" + dimension + "'."); } return Collections.singletonList(user); }); dimensionLong(ID).description("internal user id") .filter((value, item) -> value.equals(item.getId())) .toItems(dimension -> { SUser user = myUserModel.findUserById(dimension); if (user == null) { throw new NotFoundException("No user can be found by id '" + dimension + "'."); } return Collections.singletonList(user); }); dimensionString(USERNAME).description("user's username") .filter((value, item) -> value.equalsIgnoreCase(item.getUsername())) .toItems(dimension -> { SUser user = myUserModel.findUserAccount(null, dimension); if (user == null) { throw new NotFoundException("No user can be found by username '" + dimension + "'."); } return Collections.singletonList(user); }); final Type myGroupMapper = type(dimensionValue -> myGroupFinder.getGroup(dimensionValue)).description("user groups locator"); dimension(GROUP, myGroupMapper).description("user group including the user directly") .filter((value, item) -> value.containsUserDirectly(item)) .toItems(dimension -> { checkViewAllUsersPermissionEnforced(); return convert(dimension.getDirectUsers()); }); dimension(AFFECTED_GROUP, myGroupMapper).description("user group including the user considering groups hierarchy") .filter((value, item) -> item.getAllUserGroups().contains(value)) .toItems(dimension -> { checkViewAllUsersPermissionEnforced(); return convert(dimension.getAllUsers()); }); dimensionParameterCondition(PROPERTY).description("user's property").valueForDefaultFilter(item -> getUserPropertiesProvider(item)); dimensionValueCondition(EMAIL).description("user's email").valueForDefaultFilter(item -> item.getEmail()); dimensionValueCondition(NAME).description("user's display name").valueForDefaultFilter(item -> item.getName()); dimensionBoolean(HAS_PASSWORD).description("user has not empty password").hidden().valueForDefaultFilter(item -> ((UserImpl)item).hasPassword()); dimension(PASSWORD, type(dimensionValue -> { if (!myPermissionChecker.isPermissionGranted(Permission.CHANGE_SERVER_SETTINGS, null)) { throw new AuthorizationFailedException("Only system admin can query users for passwords"); } try { Thread.sleep(TeamCityProperties.getInteger("rest.request.users.passwordCheckDelay.ms", 5 * 1000)); //inapt attempt to prevent brute-forcing } catch (InterruptedException e) { //ignore } return dimensionValue; }).description("text")).description("user's password") .hidden() .filter((value, item) -> myUserModel.findUserAccount(item.getRealm(), item.getUsername(), value) != null); dimensionTimeCondition(LAST_LOGIN_TIME, myTimeCondition).description("user's last login time").valueForDefaultFilter(item -> item.getLastLoginTimestamp()); dimension(ROLE, type(dimensionValue -> new RoleEntryDatas(dimensionValue, myRolesManager, myProjectFinder, myPermissionChecker)).description("role locator")) .description("user's role") .filter((roleEntryDatas, item) -> roleEntryDatas.matches(item)); PermissionCheck permissionCheck = new PermissionCheck(); dimension(PERMISSION, type(dimensionValue -> permissionCheck.matches(dimensionValue)).description("permission check locator")) .description("user's permission (experimental)").hidden() .filter((type, item) -> type.isIncluded(item)); multipleConvertToItemHolder(DimensionCondition.ALWAYS, dimensions -> { checkViewAllUsersPermissionEnforced(); return getItemHolder(myUserModel.getAllUsers().getUsers()); }); locatorProvider(user -> getLocator(user)); containerSetProvider(() -> new HashSet()); } } private class PermissionCheck { private final Dimension> PROJECT = new Dimension<>("project"); private final Dimension PERMISSION = new Dimension<>("permission"); private final Finder myFinder; PermissionCheck() { TypedFinderBuilder builder = new TypedFinderBuilder(); builder.dimensionProjects(PROJECT, myServiceLocator).description("project to check permission in (matching when permission is present at least in one of the matched projects), when omitted checking globally"); builder.dimensionEnum(PERMISSION, Permission.class).description("permission to check, should be present"); builder.filter(locator -> locator.lookupSingleDimensionValue(PERMISSION.name) != null && locator.lookupDimensionValue(PROJECT.name).size() <= 1, dimensions -> new UserPermissionFilter(dimensions)); myFinder = builder.build(); } ItemFilter matches(@NotNull String permissionCheckLocator) { return myFinder.getFilter(permissionCheckLocator); } private class UserPermissionFilter implements ItemFilter { private final Permission myPermission; private final List myProjects; UserPermissionFilter(final TypedFinderBuilder.DimensionObjects dimensions) { //noinspection ConstantConditions - is checked in a filter condition earlier myPermission = dimensions.get(PERMISSION).get(0); List> projects = dimensions.get(PROJECT); myProjects = projects == null ? null : projects.get(0); } @Override public boolean shouldStop(@NotNull final SUser item) { return false; } @Override public boolean isIncluded(@NotNull final SUser item) { return PermissionChecker.anyOfUsersHavePermissionForAnyOfProjects(Collections.singletonList(item), myPermission, myProjects); } } } }