/*
* Copyright 2017 ThoughtWorks, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.thoughtworks.go.server.service;
import com.thoughtworks.go.config.*;
import com.thoughtworks.go.domain.*;
import com.thoughtworks.go.domain.Users;
import com.thoughtworks.go.domain.exception.ValidationException;
import com.thoughtworks.go.i18n.LocalizedMessage;
import com.thoughtworks.go.presentation.TriStateSelection;
import com.thoughtworks.go.presentation.UserModel;
import com.thoughtworks.go.presentation.UserSearchModel;
import com.thoughtworks.go.presentation.UserSourceType;
import com.thoughtworks.go.server.dao.UserDao;
import com.thoughtworks.go.server.domain.Username;
import com.thoughtworks.go.server.exceptions.UserEnabledException;
import com.thoughtworks.go.server.exceptions.UserNotFoundException;
import com.thoughtworks.go.server.persistence.OauthRepository;
import com.thoughtworks.go.server.security.OnlyKnownUsersAllowedException;
import com.thoughtworks.go.server.service.result.HttpLocalizedOperationResult;
import com.thoughtworks.go.server.service.result.LocalizedOperationResult;
import com.thoughtworks.go.server.transaction.TransactionTemplate;
import com.thoughtworks.go.serverhealth.HealthStateScope;
import com.thoughtworks.go.serverhealth.HealthStateType;
import com.thoughtworks.go.util.Filter;
import com.thoughtworks.go.util.TriState;
import com.thoughtworks.go.util.comparator.AlphaAsciiCollectionComparator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import java.util.*;
@Service
public class UserService {
private final UserDao userDao;
private final SecurityService securityService;
private final GoConfigService goConfigService;
private final OauthRepository oauthRepository;
private final TransactionTemplate transactionTemplate;
private final Object disableUserMutex = new Object();
private final Object enableUserMutex = new Object();
@Autowired
public UserService(UserDao userDao, SecurityService securityService, GoConfigService goConfigService, TransactionTemplate transactionTemplate,
OauthRepository oauthRepository) {
this.userDao = userDao;
this.securityService = securityService;
this.goConfigService = goConfigService;
this.transactionTemplate = transactionTemplate;
this.oauthRepository = oauthRepository;
}
public void deleteAll() {
userDao.deleteAll();
}
public void disable(final List<String> usersToBeDisabled, LocalizedOperationResult result) {
synchronized (disableUserMutex) {
if (willDisableAllAdmins(usersToBeDisabled)) {
result.badRequest(LocalizedMessage.string("CANNOT_DISABLE_LAST_ADMIN"));
return;
}
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
userDao.disableUsers(usersToBeDisabled);
oauthRepository.deleteUsersOauthGrants(usersToBeDisabled);
}
});
}
}
public boolean canUserTurnOffAutoLogin() {
return !willDisableAllAdmins(new ArrayList<>());
}
private boolean willDisableAllAdmins(List<String> usersToBeDisabled) {
List<String> enabledUserNames = toUserNames(userDao.enabledUsers());
enabledUserNames.removeAll(usersToBeDisabled);
return !userNameListContainsAdmin(enabledUserNames);
}
private List<String> toUserNames(List<User> enabledUsers) {
List<String> enabledUserNames = new ArrayList<>();
for (User enabledUser : enabledUsers) {
enabledUserNames.add(enabledUser.getName());
}
return enabledUserNames;
}
private boolean userNameListContainsAdmin(List<String> enabledUserNames) {
for (String enabledUserName : enabledUserNames) {
if (securityService.isUserAdmin(new Username(new CaseInsensitiveString(enabledUserName)))) {
return true;
}
}
return false;
}
public User save(final User user, TriState enabled, TriState emailMe, String email, String checkinAliases, LocalizedOperationResult result) {
if (enabled.isTrue()) {
user.enable();
}
if (enabled.isFalse()) {
user.disable();
}
if (email != null) {
user.setEmail(email);
}
if (checkinAliases != null) {
user.setMatcher(checkinAliases);
}
if (emailMe.isTrue()) {
user.setEmailMe(true);
}
if (emailMe.isFalse()) {
user.setEmailMe(false);
}
if (validate(result, user)) {
return user;
}
try {
saveOrUpdate(user);
} catch (ValidationException e) {
result.badRequest(LocalizedMessage.string("USER_FIELD_VALIDATIONS_FAILED", e.getMessage()));
}
return user;
}
public void enable(List<String> usernames, LocalizedOperationResult result) {
synchronized (enableUserMutex) {
Set<String> potentialEnabledUsers = new HashSet<>(toUserNames(userDao.enabledUsers()));
potentialEnabledUsers.addAll(usernames);
userDao.enableUsers(usernames);
}
}
public int enabledUserCount() {
return userDao.enabledUserCount();
}
public int disabledUserCount() {
return allUsersForDisplay().size() - enabledUserCount();
}
public void modifyRolesAndUserAdminPrivileges(final List<String> users, final TriStateSelection adminPrivilege, final List<TriStateSelection> roleSelections, LocalizedOperationResult result) {
Users allUsers = userDao.allUsers();
for (String user : users) {
if (!allUsers.containsUserNamed(user)) {
result.badRequest(LocalizedMessage.string("USER_DOES_NOT_EXIST_IN_DB", user));
return;
}
}
try {
final GoConfigDao.CompositeConfigCommand command = new GoConfigDao.CompositeConfigCommand();
command.addCommand(goConfigService.modifyRolesCommand(users, roleSelections));
command.addCommand(goConfigService.modifyAdminPrivilegesCommand(users, adminPrivilege));
goConfigService.updateConfig(command);
} catch (Exception e) {
result.badRequest(LocalizedMessage.string("INVALID_ROLE_NAME", e.getMessage()));
}
}
public Set<String> allUsernames() {
List<UserModel> userModels = allUsersForDisplay();
Set<String> users = new HashSet<>();
for (UserModel model : userModels) {
users.add(model.getUser().getName());
}
return users;
}
public Collection<String> allRoleNames(CruiseConfig cruiseConfig) {
List<String> roles = new ArrayList<>();
for (Role role : allRoles(cruiseConfig)) {
roles.add(CaseInsensitiveString.str(role.getName()));
}
return roles;
}
public Collection<String> allRoleNames() {
return allRoleNames(goConfigService.cruiseConfig());
}
public Collection<Role> allRoles(CruiseConfig cruiseConfig) {
return cruiseConfig.server().security().getRoles();
}
public Set<String> usersThatCanOperateOnStage(CruiseConfig cruiseConfig, PipelineConfig pipelineConfig) {
SortedSet<String> users = new TreeSet<>();
PipelineConfigs group = cruiseConfig.findGroupOfPipeline(pipelineConfig);
if (group.hasAuthorizationDefined()) {
if (group.hasOperationPermissionDefined()) {
users.addAll(group.getOperateUserNames());
List<String> roles = group.getOperateRoleNames();
for (Role role : cruiseConfig.server().security().getRoles()) {
if (roles.contains(CaseInsensitiveString.str(role.getName()))) {
users.addAll(role.usersOfRole());
}
}
}
} else {
users.addAll(allUsernames());
}
return users;
}
public Set<String> rolesThatCanOperateOnStage(CruiseConfig cruiseConfig, PipelineConfig pipelineConfig) {
PipelineConfigs group = cruiseConfig.findGroupOfPipeline(pipelineConfig);
SortedSet<String> roles = new TreeSet<>();
if (group.hasAuthorizationDefined()) {
if (group.hasOperationPermissionDefined()) {
roles.addAll(group.getOperateRoleNames());
}
} else {
roles.addAll(allRoleNames(cruiseConfig));
}
return roles;
}
public User load(long id) {
return userDao.load(id);
}
public void deleteUser(String username, HttpLocalizedOperationResult result) {
try {
userDao.deleteUser(username);
result.setMessage(LocalizedMessage.string("RESOURCE_DELETE_SUCCESSFUL", "user", username));
} catch (UserNotFoundException e) {
result.notFound(LocalizedMessage.string("RESOURCE_NOT_FOUND", "User", username), HealthStateType.general(HealthStateScope.GLOBAL));
} catch (UserEnabledException e) {
result.badRequest(LocalizedMessage.string("USER_NOT_DISABLED", username));
}
}
public enum SortableColumn {
EMAIL {
protected String get(UserModel model) {
return model.getUser().getEmail();
}
},
USERNAME {
protected String get(UserModel model) {
return model.getUser().getName();
}
},
ROLES {
@Override
public Comparator<UserModel> sorter() {
return new Comparator<UserModel>() {
public int compare(UserModel one, UserModel other) {
return STRING_COMPARATOR.compare(one.getRoles(), other.getRoles());
}
};
}
},
MATCHERS {
@Override
public Comparator<UserModel> sorter() {
return new Comparator<UserModel>() {
public int compare(UserModel one, UserModel other) {
return STRING_COMPARATOR.compare(one.getUser().getMatchers(), other.getUser().getMatchers());
}
};
}
},
IS_ADMIN {
@Override
public Comparator<UserModel> sorter() {
return new Comparator<UserModel>() {
public int compare(UserModel one, UserModel other) {
return ((Boolean) one.isAdmin()).compareTo(other.isAdmin());
}
};
}
},
ENABLED {
@Override
public Comparator<UserModel> sorter() {
return new Comparator<UserModel>() {
public int compare(UserModel one, UserModel other) {
return ((Boolean) one.isEnabled()).compareTo(other.isEnabled());
}
};
}
};
private static final AlphaAsciiCollectionComparator<String> STRING_COMPARATOR = new AlphaAsciiCollectionComparator<>();
public Comparator<UserModel> sorter() {
return new Comparator<UserModel>() {
public int compare(UserModel one, UserModel other) {
return get(one).compareTo(get(other));
}
};
}
protected String get(UserModel model) {
return null;
}
}
public enum SortDirection {
ASC {
@Override
public Comparator<UserModel> forColumn(final SortableColumn column) {
return column.sorter();
}
},
DESC {
@Override
public Comparator<UserModel> forColumn(final SortableColumn column) {
return new Comparator<UserModel>() {
public int compare(UserModel one, UserModel other) {
return column.sorter().compare(other, one);
}
};
}
};
public abstract Comparator<UserModel> forColumn(SortableColumn column);
}
public void addUserIfDoesNotExist(User user) {
synchronized (enableUserMutex) {
if (!(user.isAnonymous() || userExists(user))) {
assertUnknownUsersAreAllowedToLogin();
userDao.saveOrUpdate(user);
}
}
}
public void withEnableUserMutex(Runnable runnable) {
synchronized (enableUserMutex) {
runnable.run();
}
}
private void assertUnknownUsersAreAllowedToLogin() {
if (goConfigService.isOnlyKnownUserAllowedToLogin()) {
throw new OnlyKnownUsersAllowedException("Please ask the administrator to add you to Go");
}
}
public void saveOrUpdate(User user) throws ValidationException {
validate(user);
synchronized (enableUserMutex) {
userDao.saveOrUpdate(user);
}
}
private boolean userExists(User user) {
User foundUser = userDao.findUser(user.getName());
return !(foundUser instanceof NullUser);
}
public User findUserByName(String username) {
return userDao.findUser(username);
}
public void addNotificationFilter(final long userId, final NotificationFilter filter) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
User user = userDao.load(userId);
user.addNotificationFilter(filter);
synchronized (enableUserMutex) {
userDao.saveOrUpdate(user);
}
}
});
}
public void removeNotificationFilter(final long userId, final long filterId) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
User user = userDao.load(userId);
user.removeNotificationFilter(filterId);
userDao.saveOrUpdate(user);
}
});
}
public Users findValidSubscribers(final StageConfigIdentifier identifier) {
Users users = userDao.findNotificationSubscribingUsers();
return users.filter(new Filter<User>() {
public boolean matches(User user) {
return user.hasSubscribedFor(identifier.getPipelineName(), identifier.getStageName()) &&
securityService.hasViewPermissionForPipeline(user.getUsername(), identifier.getPipelineName());
}
});
}
public void validate(User user) throws ValidationException {
user.validateLoginName();
user.validateMatcher();
user.validateEmail();
}
public List<UserModel> allUsersForDisplay(SortableColumn column, SortDirection direction) {
List<UserModel> userModels = allUsersForDisplay();
Comparator<UserModel> userModelComparator = direction.forColumn(column);
Collections.sort(userModels, userModelComparator);
return userModels;
}
private List<UserModel> allUsersForDisplay() {
Collection<User> users = allUsers();
ArrayList<UserModel> userModels = new ArrayList<>();
for (User user : users) {
String userName = user.getName();
ArrayList<String> roles = new ArrayList<>();
for (Role role : goConfigService.rolesForUser(new CaseInsensitiveString(userName))) {
roles.add(CaseInsensitiveString.str(role.getName()));
}
userModels.add(new UserModel(user, roles, securityService.isUserAdmin(new Username(new CaseInsensitiveString(userName)))));
}
return userModels;
}
public Collection<User> allUsers() {
Set<User> result = new HashSet<>();
result.addAll(userDao.allUsers());
return result;
}
public void create(List<UserSearchModel> userSearchModels, HttpLocalizedOperationResult result) {
if (userSearchModels.isEmpty()) {
result.badRequest(LocalizedMessage.string("NO_USERS_SELECTED"));
return;
}
synchronized (enableUserMutex) {
for (UserSearchModel userSearchModel : userSearchModels) {
User user = userSearchModel.getUser();
if (userExists(user)) {
result.conflict(LocalizedMessage.string("RESOURCE_ALREADY_EXISTS", "user", user.getName(), user.getDisplayName(), user.getEmail()));
return;
}
if (user.isAnonymous()) {
result.badRequest(LocalizedMessage.string("USERNAME_NOT_PERMITTED", user.getName()));
return;
}
if (!userSearchModel.getUserSourceType().equals(UserSourceType.PASSWORD_FILE) && validate(result, user)) {
return;
}
userDao.saveOrUpdate(user);
result.setMessage(LocalizedMessage.string("USER_SUCCESSFULLY_ADDED", user.getName()));
}
}
}
public static class AdminAndRoleSelections {
private final TriStateSelection adminSelection;
private final List<TriStateSelection> roleSelections;
public AdminAndRoleSelections(TriStateSelection adminSelection, List<TriStateSelection> roleSelections) {
this.adminSelection = adminSelection;
this.roleSelections = roleSelections;
}
public TriStateSelection getAdminSelection() {
return adminSelection;
}
public List<TriStateSelection> getRoleSelections() {
return roleSelections;
}
}
public AdminAndRoleSelections getAdminAndRoleSelections(List<String> users) {
final SecurityConfig securityConfig = goConfigService.security();
Set<Role> roles = new HashSet<>(securityConfig.getRoles().getRoleConfigs());
final List<TriStateSelection> roleSelections = TriStateSelection.forRoles(roles, users);
final TriStateSelection adminSelection = TriStateSelection.forSystemAdmin(securityConfig.adminsConfig(), roles, new SecurityService.UserRoleMatcherImpl(securityConfig),
users);
return new AdminAndRoleSelections(adminSelection, roleSelections);
}
private boolean validate(LocalizedOperationResult result, User user) {
try {
validate(user);
} catch (ValidationException e) {
result.badRequest(LocalizedMessage.string("USER_FIELD_VALIDATIONS_FAILED", e.getMessage()));
return true;
}
return false;
}
}