You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

UserUpdater.java 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.user;
  21. import com.google.common.base.Joiner;
  22. import com.google.common.base.Strings;
  23. import java.util.ArrayList;
  24. import java.util.HashSet;
  25. import java.util.List;
  26. import java.util.Objects;
  27. import java.util.function.Consumer;
  28. import java.util.regex.Pattern;
  29. import java.util.stream.Collectors;
  30. import javax.annotation.Nullable;
  31. import javax.inject.Inject;
  32. import org.apache.commons.lang.math.RandomUtils;
  33. import org.sonar.api.config.Configuration;
  34. import org.sonar.api.platform.NewUserHandler;
  35. import org.sonar.api.server.ServerSide;
  36. import org.sonar.db.DbClient;
  37. import org.sonar.db.DbSession;
  38. import org.sonar.db.audit.AuditPersister;
  39. import org.sonar.db.audit.model.SecretNewValue;
  40. import org.sonar.db.user.GroupDto;
  41. import org.sonar.db.user.UserDto;
  42. import org.sonar.db.user.UserGroupDto;
  43. import org.sonar.server.authentication.CredentialsLocalAuthentication;
  44. import org.sonar.server.usergroups.DefaultGroupFinder;
  45. import org.sonar.server.util.Validation;
  46. import static com.google.common.base.Preconditions.checkArgument;
  47. import static com.google.common.base.Strings.isNullOrEmpty;
  48. import static com.google.common.collect.Lists.newArrayList;
  49. import static java.lang.String.format;
  50. import static java.util.Collections.emptyList;
  51. import static org.sonar.api.CoreProperties.DEFAULT_ISSUE_ASSIGNEE;
  52. import static org.sonar.core.util.Slug.slugify;
  53. import static org.sonar.server.exceptions.BadRequestException.checkRequest;
  54. @ServerSide
  55. public class UserUpdater {
  56. private static final String SQ_AUTHORITY = "sonarqube";
  57. private static final String LOGIN_PARAM = "Login";
  58. private static final String PASSWORD_PARAM = "Password";
  59. private static final String NAME_PARAM = "Name";
  60. private static final String EMAIL_PARAM = "Email";
  61. private static final Pattern START_WITH_SPECIFIC_AUTHORIZED_CHARACTERS = Pattern.compile("\\w+");
  62. private static final Pattern CONTAINS_ONLY_AUTHORIZED_CHARACTERS = Pattern.compile("\\A\\w[\\w\\.\\-@]+\\z");
  63. public static final int LOGIN_MIN_LENGTH = 2;
  64. public static final int LOGIN_MAX_LENGTH = 255;
  65. public static final int EMAIL_MAX_LENGTH = 100;
  66. public static final int NAME_MAX_LENGTH = 200;
  67. private final NewUserNotifier newUserNotifier;
  68. private final DbClient dbClient;
  69. private final DefaultGroupFinder defaultGroupFinder;
  70. private final AuditPersister auditPersister;
  71. private final CredentialsLocalAuthentication localAuthentication;
  72. @Inject
  73. public UserUpdater(NewUserNotifier newUserNotifier, DbClient dbClient, DefaultGroupFinder defaultGroupFinder, Configuration config,
  74. AuditPersister auditPersister, CredentialsLocalAuthentication localAuthentication) {
  75. this.newUserNotifier = newUserNotifier;
  76. this.dbClient = dbClient;
  77. this.defaultGroupFinder = defaultGroupFinder;
  78. this.auditPersister = auditPersister;
  79. this.localAuthentication = localAuthentication;
  80. }
  81. public UserDto createAndCommit(DbSession dbSession, NewUser newUser, Consumer<UserDto> beforeCommit, UserDto... otherUsersToIndex) {
  82. UserDto userDto = saveUser(dbSession, createDto(dbSession, newUser));
  83. return commitUser(dbSession, userDto, beforeCommit, otherUsersToIndex);
  84. }
  85. public UserDto reactivateAndCommit(DbSession dbSession, UserDto disabledUser, NewUser newUser, Consumer<UserDto> beforeCommit, UserDto... otherUsersToIndex) {
  86. checkArgument(!disabledUser.isActive(), "An active user with login '%s' already exists", disabledUser.getLogin());
  87. reactivateUser(dbSession, disabledUser, newUser);
  88. return commitUser(dbSession, disabledUser, beforeCommit, otherUsersToIndex);
  89. }
  90. private void reactivateUser(DbSession dbSession, UserDto reactivatedUser, NewUser newUser) {
  91. UpdateUser updateUser = new UpdateUser()
  92. .setName(newUser.name())
  93. .setEmail(newUser.email())
  94. .setScmAccounts(newUser.scmAccounts())
  95. .setExternalIdentity(newUser.externalIdentity());
  96. String login = newUser.login();
  97. if (login != null) {
  98. updateUser.setLogin(login);
  99. }
  100. String password = newUser.password();
  101. if (password != null) {
  102. updateUser.setPassword(password);
  103. }
  104. updateDto(dbSession, updateUser, reactivatedUser);
  105. updateUser(dbSession, reactivatedUser);
  106. addUserToDefaultGroup(dbSession, reactivatedUser);
  107. }
  108. public void updateAndCommit(DbSession dbSession, UserDto dto, UpdateUser updateUser, Consumer<UserDto> beforeCommit, UserDto... otherUsersToIndex) {
  109. boolean isUserUpdated = updateDto(dbSession, updateUser, dto);
  110. if (isUserUpdated) {
  111. // at least one change. Database must be updated and Elasticsearch re-indexed
  112. updateUser(dbSession, dto);
  113. commitUser(dbSession, dto, beforeCommit, otherUsersToIndex);
  114. } else {
  115. // no changes but still execute the consumer
  116. beforeCommit.accept(dto);
  117. dbSession.commit();
  118. }
  119. }
  120. private UserDto commitUser(DbSession dbSession, UserDto userDto, Consumer<UserDto> beforeCommit, UserDto... otherUsersToIndex) {
  121. beforeCommit.accept(userDto);
  122. dbSession.commit();
  123. notifyNewUser(userDto.getLogin(), userDto.getName(), userDto.getEmail());
  124. return userDto;
  125. }
  126. private UserDto createDto(DbSession dbSession, NewUser newUser) {
  127. UserDto userDto = new UserDto();
  128. List<String> messages = new ArrayList<>();
  129. String login = newUser.login();
  130. if (isNullOrEmpty(login)) {
  131. userDto.setLogin(generateUniqueLogin(dbSession, newUser.name()));
  132. } else if (validateLoginFormat(login, messages)) {
  133. checkLoginUniqueness(dbSession, login);
  134. userDto.setLogin(login);
  135. }
  136. String name = newUser.name();
  137. if (validateNameFormat(name, messages)) {
  138. userDto.setName(name);
  139. }
  140. String email = newUser.email();
  141. if (email != null && validateEmailFormat(email, messages)) {
  142. userDto.setEmail(email);
  143. }
  144. String password = newUser.password();
  145. if (password != null && validatePasswords(password, messages)) {
  146. localAuthentication.storeHashPassword(userDto, password);
  147. }
  148. List<String> scmAccounts = sanitizeScmAccounts(newUser.scmAccounts());
  149. if (scmAccounts != null && !scmAccounts.isEmpty() && validateScmAccounts(dbSession, scmAccounts, login, email, null, messages)) {
  150. userDto.setScmAccounts(scmAccounts);
  151. }
  152. setExternalIdentity(dbSession, userDto, newUser.externalIdentity());
  153. checkRequest(messages.isEmpty(), messages);
  154. return userDto;
  155. }
  156. private String generateUniqueLogin(DbSession dbSession, String userName) {
  157. String slugName = slugify(userName);
  158. for (int i = 0; i < 10; i++) {
  159. String login = slugName + RandomUtils.nextInt(100_000);
  160. UserDto existingUser = dbClient.userDao().selectByLogin(dbSession, login);
  161. if (existingUser == null) {
  162. return login;
  163. }
  164. }
  165. throw new IllegalStateException("Cannot create unique login for user name " + userName);
  166. }
  167. private boolean updateDto(DbSession dbSession, UpdateUser update, UserDto dto) {
  168. List<String> messages = newArrayList();
  169. boolean changed = updateLogin(dbSession, update, dto, messages);
  170. changed |= updateName(update, dto, messages);
  171. changed |= updateEmail(update, dto, messages);
  172. changed |= updateExternalIdentity(dbSession, update, dto);
  173. changed |= updatePassword(dbSession, update, dto, messages);
  174. changed |= updateScmAccounts(dbSession, update, dto, messages);
  175. checkRequest(messages.isEmpty(), messages);
  176. return changed;
  177. }
  178. private boolean updateLogin(DbSession dbSession, UpdateUser updateUser, UserDto userDto, List<String> messages) {
  179. String newLogin = updateUser.login();
  180. if (!updateUser.isLoginChanged() || !validateLoginFormat(newLogin, messages) || Objects.equals(userDto.getLogin(), newLogin)) {
  181. return false;
  182. }
  183. checkLoginUniqueness(dbSession, newLogin);
  184. dbClient.propertiesDao().selectByKeyAndMatchingValue(dbSession, DEFAULT_ISSUE_ASSIGNEE, userDto.getLogin())
  185. .forEach(p -> dbClient.propertiesDao().saveProperty(p.setValue(newLogin)));
  186. userDto.setLogin(newLogin);
  187. if (userDto.isLocal() || SQ_AUTHORITY.equals(userDto.getExternalIdentityProvider())) {
  188. userDto.setExternalLogin(newLogin);
  189. userDto.setExternalId(newLogin);
  190. }
  191. return true;
  192. }
  193. private static boolean updateName(UpdateUser updateUser, UserDto userDto, List<String> messages) {
  194. String name = updateUser.name();
  195. if (updateUser.isNameChanged() && validateNameFormat(name, messages) && !Objects.equals(userDto.getName(), name)) {
  196. userDto.setName(name);
  197. return true;
  198. }
  199. return false;
  200. }
  201. private static boolean updateEmail(UpdateUser updateUser, UserDto userDto, List<String> messages) {
  202. String email = updateUser.email();
  203. if (updateUser.isEmailChanged() && validateEmailFormat(email, messages) && !Objects.equals(userDto.getEmail(), email)) {
  204. userDto.setEmail(email);
  205. return true;
  206. }
  207. return false;
  208. }
  209. private boolean updateExternalIdentity(DbSession dbSession, UpdateUser updateUser, UserDto userDto) {
  210. ExternalIdentity externalIdentity = updateUser.externalIdentity();
  211. if (updateUser.isExternalIdentityChanged() && !isSameExternalIdentity(userDto, externalIdentity)) {
  212. setExternalIdentity(dbSession, userDto, externalIdentity);
  213. return true;
  214. }
  215. return false;
  216. }
  217. private boolean updatePassword(DbSession dbSession, UpdateUser updateUser, UserDto userDto, List<String> messages) {
  218. String password = updateUser.password();
  219. if (updateUser.isPasswordChanged() && validatePasswords(password, messages) && checkPasswordChangeAllowed(userDto, messages)) {
  220. localAuthentication.storeHashPassword(userDto, password);
  221. userDto.setResetPassword(false);
  222. auditPersister.updateUserPassword(dbSession, new SecretNewValue("userLogin", userDto.getLogin()));
  223. return true;
  224. }
  225. return false;
  226. }
  227. private boolean updateScmAccounts(DbSession dbSession, UpdateUser updateUser, UserDto userDto, List<String> messages) {
  228. String email = updateUser.email();
  229. List<String> scmAccounts = sanitizeScmAccounts(updateUser.scmAccounts());
  230. List<String> existingScmAccounts = userDto.getSortedScmAccounts();
  231. if (updateUser.isScmAccountsChanged() && !(existingScmAccounts.containsAll(scmAccounts) && scmAccounts.containsAll(existingScmAccounts))) {
  232. if (!scmAccounts.isEmpty()) {
  233. String newOrOldEmail = email != null ? email : userDto.getEmail();
  234. if (validateScmAccounts(dbSession, scmAccounts, userDto.getLogin(), newOrOldEmail, userDto, messages)) {
  235. userDto.setScmAccounts(scmAccounts);
  236. }
  237. } else {
  238. userDto.setScmAccounts(emptyList());
  239. }
  240. return true;
  241. }
  242. return false;
  243. }
  244. private static boolean isSameExternalIdentity(UserDto dto, @Nullable ExternalIdentity externalIdentity) {
  245. return externalIdentity != null
  246. && !dto.isLocal()
  247. && Objects.equals(dto.getExternalId(), externalIdentity.getId())
  248. && Objects.equals(dto.getExternalLogin(), externalIdentity.getLogin())
  249. && Objects.equals(dto.getExternalIdentityProvider(), externalIdentity.getProvider());
  250. }
  251. private void setExternalIdentity(DbSession dbSession, UserDto dto, @Nullable ExternalIdentity externalIdentity) {
  252. if (externalIdentity == null) {
  253. dto.setExternalLogin(dto.getLogin());
  254. dto.setExternalIdentityProvider(SQ_AUTHORITY);
  255. dto.setExternalId(dto.getLogin());
  256. dto.setLocal(true);
  257. } else {
  258. dto.setExternalLogin(externalIdentity.getLogin());
  259. dto.setExternalIdentityProvider(externalIdentity.getProvider());
  260. dto.setExternalId(externalIdentity.getId());
  261. dto.setLocal(false);
  262. dto.setSalt(null);
  263. dto.setCryptedPassword(null);
  264. }
  265. UserDto existingUser = dbClient.userDao().selectByExternalIdAndIdentityProvider(dbSession, dto.getExternalId(), dto.getExternalIdentityProvider());
  266. checkArgument(existingUser == null || Objects.equals(dto.getUuid(), existingUser.getUuid()),
  267. "A user with provider id '%s' and identity provider '%s' already exists", dto.getExternalId(), dto.getExternalIdentityProvider());
  268. }
  269. private static boolean checkNotEmptyParam(@Nullable String value, String param, List<String> messages) {
  270. if (isNullOrEmpty(value)) {
  271. messages.add(format(Validation.CANT_BE_EMPTY_MESSAGE, param));
  272. return false;
  273. }
  274. return true;
  275. }
  276. private static boolean validateLoginFormat(@Nullable String login, List<String> messages) {
  277. boolean isValid = checkNotEmptyParam(login, LOGIN_PARAM, messages);
  278. if (isValid) {
  279. if (login.length() < LOGIN_MIN_LENGTH) {
  280. messages.add(format(Validation.IS_TOO_SHORT_MESSAGE, LOGIN_PARAM, LOGIN_MIN_LENGTH));
  281. return false;
  282. } else if (login.length() > LOGIN_MAX_LENGTH) {
  283. messages.add(format(Validation.IS_TOO_LONG_MESSAGE, LOGIN_PARAM, LOGIN_MAX_LENGTH));
  284. return false;
  285. } else if (!startWithUnderscoreOrAlphanumeric(login)) {
  286. messages.add("Login should start with _ or alphanumeric.");
  287. return false;
  288. } else if (!CONTAINS_ONLY_AUTHORIZED_CHARACTERS.matcher(login).matches()) {
  289. messages.add("Login should contain only letters, numbers, and .-_@");
  290. return false;
  291. }
  292. }
  293. return isValid;
  294. }
  295. private static boolean startWithUnderscoreOrAlphanumeric(String login) {
  296. String firstCharacter = login.substring(0, 1);
  297. if ("_".equals(firstCharacter)) {
  298. return true;
  299. }
  300. return START_WITH_SPECIFIC_AUTHORIZED_CHARACTERS.matcher(firstCharacter).matches();
  301. }
  302. private static boolean validateNameFormat(@Nullable String name, List<String> messages) {
  303. boolean isValid = checkNotEmptyParam(name, NAME_PARAM, messages);
  304. if (name != null && name.length() > NAME_MAX_LENGTH) {
  305. messages.add(format(Validation.IS_TOO_LONG_MESSAGE, NAME_PARAM, 200));
  306. return false;
  307. }
  308. return isValid;
  309. }
  310. private static boolean validateEmailFormat(@Nullable String email, List<String> messages) {
  311. if (email != null && email.length() > EMAIL_MAX_LENGTH) {
  312. messages.add(format(Validation.IS_TOO_LONG_MESSAGE, EMAIL_PARAM, 100));
  313. return false;
  314. }
  315. return true;
  316. }
  317. private static boolean checkPasswordChangeAllowed(UserDto userDto, List<String> messages) {
  318. if (!userDto.isLocal()) {
  319. messages.add("Password cannot be changed when external authentication is used");
  320. return false;
  321. }
  322. return true;
  323. }
  324. private static boolean validatePasswords(@Nullable String password, List<String> messages) {
  325. if (password == null || password.length() == 0) {
  326. messages.add(format(Validation.CANT_BE_EMPTY_MESSAGE, PASSWORD_PARAM));
  327. return false;
  328. }
  329. return true;
  330. }
  331. private boolean validateScmAccounts(DbSession dbSession, List<String> scmAccounts, @Nullable String login, @Nullable String email, @Nullable UserDto existingUser,
  332. List<String> messages) {
  333. boolean isValid = true;
  334. for (String scmAccount : scmAccounts) {
  335. if (scmAccount.equals(login) || scmAccount.equals(email)) {
  336. messages.add("Login and email are automatically considered as SCM accounts");
  337. isValid = false;
  338. } else {
  339. List<UserDto> matchingUsers = dbClient.userDao().selectByScmAccountOrLoginOrEmail(dbSession, scmAccount);
  340. List<String> matchingUsersWithoutExistingUser = newArrayList();
  341. for (UserDto matchingUser : matchingUsers) {
  342. if (existingUser != null && matchingUser.getUuid().equals(existingUser.getUuid())) {
  343. continue;
  344. }
  345. matchingUsersWithoutExistingUser.add(getNameOrLogin(matchingUser) + " (" + matchingUser.getLogin() + ")");
  346. }
  347. if (!matchingUsersWithoutExistingUser.isEmpty()) {
  348. messages.add(format("The scm account '%s' is already used by user(s) : '%s'", scmAccount, Joiner.on(", ").join(matchingUsersWithoutExistingUser)));
  349. isValid = false;
  350. }
  351. }
  352. }
  353. return isValid;
  354. }
  355. private static String getNameOrLogin(UserDto user) {
  356. String name = user.getName();
  357. return name != null ? name : user.getLogin();
  358. }
  359. private static List<String> sanitizeScmAccounts(@Nullable List<String> scmAccounts) {
  360. if (scmAccounts != null) {
  361. return new HashSet<>(scmAccounts).stream()
  362. .map(Strings::emptyToNull)
  363. .filter(Objects::nonNull)
  364. .sorted(String::compareToIgnoreCase)
  365. .toList();
  366. }
  367. return emptyList();
  368. }
  369. private void checkLoginUniqueness(DbSession dbSession, String login) {
  370. UserDto existingUser = dbClient.userDao().selectByLogin(dbSession, login);
  371. checkArgument(existingUser == null, "A user with login '%s' already exists", login);
  372. }
  373. private UserDto saveUser(DbSession dbSession, UserDto userDto) {
  374. userDto.setActive(true);
  375. UserDto res = dbClient.userDao().insert(dbSession, userDto);
  376. addUserToDefaultGroup(dbSession, userDto);
  377. return res;
  378. }
  379. private void updateUser(DbSession dbSession, UserDto dto) {
  380. dto.setActive(true);
  381. dbClient.userDao().update(dbSession, dto);
  382. }
  383. private void notifyNewUser(String login, String name, @Nullable String email) {
  384. newUserNotifier.onNewUser(NewUserHandler.Context.builder()
  385. .setLogin(login)
  386. .setName(name)
  387. .setEmail(email)
  388. .build());
  389. }
  390. private static boolean isUserAlreadyMemberOfDefaultGroup(GroupDto defaultGroup, List<GroupDto> userGroups) {
  391. return userGroups.stream().anyMatch(group -> defaultGroup.getUuid().equals(group.getUuid()));
  392. }
  393. private void addUserToDefaultGroup(DbSession dbSession, UserDto userDto) {
  394. addDefaultGroup(dbSession, userDto);
  395. }
  396. private void addDefaultGroup(DbSession dbSession, UserDto userDto) {
  397. List<GroupDto> userGroups = dbClient.groupDao().selectByUserLogin(dbSession, userDto.getLogin());
  398. GroupDto defaultGroup = defaultGroupFinder.findDefaultGroup(dbSession);
  399. if (isUserAlreadyMemberOfDefaultGroup(defaultGroup, userGroups)) {
  400. return;
  401. }
  402. dbClient.userGroupDao().insert(dbSession, new UserGroupDto().setUserUuid(userDto.getUuid()).setGroupUuid(defaultGroup.getUuid()),
  403. defaultGroup.getName(), userDto.getLogin());
  404. }
  405. }