Java tutorial
/* * Licensed under MIT (https://github.com/ligoj/ligoj/blob/master/LICENSE) */ package org.ligoj.app.plugin.id.resource; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Function; import java.util.stream.Collectors; import javax.transaction.Transactional; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriInfo; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.WordUtils; import org.apache.cxf.jaxrs.impl.UriInfoImpl; import org.apache.cxf.message.Message; import org.apache.cxf.message.MessageImpl; import org.ligoj.app.api.Normalizer; import org.ligoj.app.iam.CompanyOrg; import org.ligoj.app.iam.GroupOrg; import org.ligoj.app.iam.IPasswordGenerator; import org.ligoj.app.iam.IUserRepository; import org.ligoj.app.iam.SimpleUser; import org.ligoj.app.iam.UserOrg; import org.ligoj.app.iam.dao.DelegateOrgRepository; import org.ligoj.app.iam.model.DelegateOrg; import org.ligoj.app.iam.model.DelegateType; import org.ligoj.app.plugin.id.DnUtils; import org.ligoj.app.plugin.id.dao.PasswordResetAuditRepository; import org.ligoj.app.plugin.id.model.PasswordResetAudit; import org.ligoj.bootstrap.core.json.PaginationJson; import org.ligoj.bootstrap.core.json.TableItem; import org.ligoj.bootstrap.core.json.datatable.DataTableAttributes; import org.ligoj.bootstrap.core.validation.ValidationJsonException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.ResponseBody; import lombok.extern.slf4j.Slf4j; /** * User resource. */ @Path(IdentityResource.SERVICE_URL + "/user") @Service @Produces(MediaType.APPLICATION_JSON) @Slf4j @Transactional public class UserOrgResource extends AbstractOrgResource { /** * Message key for read only resource : no "write" right. */ private static final String READ_ONLY = "read-only"; /** * Name of "group" attribute. */ private static final String GROUP = "group"; /** * The primary business key */ public static final String USER_KEY = "id"; @Autowired private DelegateOrgRepository delegateRepository; @Autowired private PasswordResetAuditRepository passwordResetRepository; @Autowired private PaginationJson paginationJson; @Autowired protected CompanyResource companyResource; @Autowired protected GroupResource groupResource; @Autowired protected ApplicationContext applicationContext; /** * Ordered columns. */ private static final Map<String, String> ORDERED_COLUMNS = new HashMap<>(); static { ORDERED_COLUMNS.put(USER_KEY, USER_KEY); ORDERED_COLUMNS.put("firstName", "firstName"); ORDERED_COLUMNS.put("lastName", "lastName"); ORDERED_COLUMNS.put("mails", "mail"); ORDERED_COLUMNS.put(SimpleUser.COMPANY_ALIAS, SimpleUser.COMPANY_ALIAS); } /** * Return users matching the given criteria. The visible groups, trees and companies are checked. The returned * groups of each user depends on the groups the user can see. The result is not secured : it contains DN. * * @param company * The optional company name to match. * @param group * The optional group name to match. * @return All matched users. */ public List<UserOrg> findAllNotSecure(final String company, final String group) { final Set<GroupOrg> visibleGroups = groupResource.getContainers(); // Search the users final MessageImpl message = new MessageImpl(); message.put(Message.QUERY_STRING, DataTableAttributes.PAGE_LENGTH + "=10000000"); return findAllNotSecure(visibleGroups, company, group, null, new UriInfoImpl(message)).getContent(); } /** * Return users matching the given criteria. The visible groups, trees and companies are checked. The returned * groups of each user depends on the groups the user can see and are in normalized CN form. The result is not * secured, it contains DN. * * @param visibleGroups * The visible groups by the principal user. * @param company * the optional company name to match. Will be normalized. * @param group * the optional group name to match. May be <code>null</code>. * @param criteria * the optional criteria to match. * @param uriInfo * filter data. * @return found users. */ private Page<UserOrg> findAllNotSecure(final Set<GroupOrg> visibleGroups, final String company, final String group, final String criteria, @Context final UriInfo uriInfo) { final PageRequest pageRequest = paginationJson.getPageRequest(uriInfo, ORDERED_COLUMNS); final Collection<String> visibleCompanies = companyResource.getContainers().stream().map(CompanyOrg::getId) .collect(Collectors.toSet()); final Map<String, GroupOrg> allGroups = getGroup().findAll(); // The companies to use final Set<String> filteredCompanies = computeFilteredCompanies(Normalizer.normalize(company), visibleCompanies); // The groups to use final Collection<GroupOrg> filteredGroups = group == null ? null : computeFilteredGroups(group, visibleGroups, allGroups); // Search the users return getUser().findAll(filteredGroups, filteredCompanies, StringUtils.trimToNull(criteria), pageRequest); } /** * Return users matching the given criteria. The visible groups, trees and companies are checked. The returned * groups of each user depends on the groups the user can see/write, and are in CN form. * * @param company * the optional company name to match. * @param group * the optional group name to match. * @param criteria * the optional criteria to match. * @param uriInfo * filter data. * @return found users. */ @GET public TableItem<UserOrgVo> findAll(@QueryParam(SimpleUser.COMPANY_ALIAS) final String company, @QueryParam(GROUP) final String group, @QueryParam(DataTableAttributes.SEARCH) final String criteria, @Context final UriInfo uriInfo) { final Set<GroupOrg> visibleGroups = groupResource.getContainers(); final Set<GroupOrg> writableGroups = groupResource.getContainersForWrite(); final Set<CompanyOrg> companies = companyResource.getContainersForWrite(); final Collection<String> writableCompanies = companies.stream().map(CompanyOrg::getId) .collect(Collectors.toList()); // Search the users final Page<UserOrg> findAll = findAllNotSecure(visibleGroups, company, group, criteria, uriInfo); // Apply pagination and secure the users data return paginationJson.applyPagination(uriInfo, findAll, rawUserOrg -> { final UserOrgVo securedUserOrg = new UserOrgVo(); rawUserOrg.copy(securedUserOrg); securedUserOrg.setCanWrite(writableCompanies.contains(rawUserOrg.getCompany())); securedUserOrg.setCanWriteGroups(!writableGroups.isEmpty()); // Show only the groups that are also visible to current user securedUserOrg.setGroups(visibleGroups.stream() .filter(mGroup -> rawUserOrg.getGroups().contains(mGroup.getId())).map(mGroup -> { final GroupVo vo = new GroupVo(); vo.setCanWrite(writableGroups.contains(mGroup)); vo.setName(mGroup.getName()); return vo; }).collect(Collectors.toList())); return securedUserOrg; }); } /** * Return a intersection of given set of visible companies and the optional requested company. */ private Set<String> computeFilteredCompanies(final String requestedCompany, final Collection<String> visibleCompanies) { // Restrict access to visible companies final Set<String> filteredCompanies; if (StringUtils.isBlank(requestedCompany)) { // No requested company, use all of them filteredCompanies = new HashSet<>(visibleCompanies); } else if (visibleCompanies.contains(requestedCompany)) { // Requested company is visible, return it filteredCompanies = Collections.singleton(requestedCompany); } else { // Requested company does not exist, result would be an empty list filteredCompanies = Collections.emptySet(); } return filteredCompanies; } /** * Computed visible groups. */ private List<GroupOrg> computeFilteredGroups(final String group, final Set<GroupOrg> visibleGroups, final Map<String, GroupOrg> allGroups) { // Restrict access to delegated groups return Optional.ofNullable(allGroups.get(Normalizer.normalize(group))) .map(fg -> allGroups.values().stream().filter(visibleGroups::contains) // Filter the group, including the children .filter(g -> DnUtils.equalsOrParentOf(fg.getDn(), g.getDn())).collect(Collectors.toList())) .orElse(Collections.emptyList()); } /** * Return a specific user from his/her login. When user does not exist or is within a non visible company, return a * 404. * * @param user * The user to find. A normalized form will be used for the search. * @return found user. Never <code>null</code>. */ @GET @Path("{user:" + SimpleUser.USER_PATTERN + "}") public UserOrg findById(@PathParam("user") final String user) { final UserOrg rawUserOrg = getUser().findByIdExpected(securityHelper.getLogin(), Normalizer.normalize(user)); // Check if the user lock status without using cache getUser().checkLockStatus(rawUserOrg); // User has been found, secure the object regarding the visible groups final UserOrg securedUserOrg = new UserOrg(); rawUserOrg.copy(securedUserOrg); // Show only the groups of user that are also visible to current user final Set<GroupOrg> visibleGroups = groupResource.getContainers(); securedUserOrg .setGroups(visibleGroups.stream().filter(mGroup -> rawUserOrg.getGroups().contains(mGroup.getId())) .sorted().map(GroupOrg::getName).collect(Collectors.toList())); return securedUserOrg; } /** * Add given user to the a group. * * @param user * The user to add. * @param group * The group to update. */ @PUT @Path("{user}/group/{group}") public void addUserToGroup(@PathParam("user") final String user, @PathParam(GROUP) final String group) { updateGroupUser(user, Normalizer.normalize(group), Collection::add); } /** * Remove given user from the a group. * * @param user * The user to remove. * @param group * The group to update. */ @DELETE @Path("{user}/group/{group}") public void removeUserFromGroup(@PathParam("user") final String user, @PathParam(GROUP) final String group) { updateGroupUser(user, Normalizer.normalize(group), Collection::remove); } /** * Performs an operation on a group and a user. * * @param user * The user to move. * @param group * The group to update. * @param updater * The function to execute on computed groups of current user. */ private void updateGroupUser(final String user, final String group, final BiPredicate<Collection<String>, String> updater) { // Get all delegates of current user final List<DelegateOrg> delegates = delegateRepository.findAllByUser(securityHelper.getLogin()); // Get the implied user final UserOrg userOrg = getUser().findByIdExpected(user); // Check the implied group validateWriteGroup(group, delegates); // Compute the new groups final Set<String> newGroups = new HashSet<>(userOrg.getGroups()); if (updater.test(newGroups, group)) { // Replace the user groups by the normalized groups including the // one we have just updated final Collection<String> mergedGroups = mergeGroups(delegates, userOrg, newGroups); // Update membership getUser().updateMembership(new ArrayList<>(mergedGroups), userOrg); } } /** * Update the given user. * * @param user * The user definition, and associated groups. Group changes are checked.User definition changes are * checked. */ @PUT public void update(final UserOrgEditionVo user) { // Check the right on the company and the groups validateChanges(securityHelper.getLogin(), user); // Check the user exists getUser().findByIdExpected(user.getId()); saveOrUpdate(user); } /** * Create the given user. * * @param user * The user definition, and associated groups. Initial groups are checked.User definition is checked. * @param quiet * Flag to turn-off the possible notification such as mail. */ public void create(final UserOrgEditionVo user, final boolean quiet) { // Check the right on the company and the groups validateChanges(securityHelper.getLogin(), user); // Check the user does not exists if (getUser().findById(user.getId()) != null) { throw new ValidationJsonException(USER_KEY, "already-exist", "0", USER_KEY, "1", user.getId()); } saveOrUpdate(user, quiet); } /** * Create the given user. * * @param user * The user definition, and associated groups. Initial groups are checked.User definition is checked. */ @POST public void create(final UserOrgEditionVo user) { create(user, false); } /** * Validate the user changes regarding the current user's right, replace group names with the exact CN, and replace * the company with a normalized one.<br> * Rules, order is important : * <ul> * <li>At least one valid delegate must exist (valid or not against the involved user). If not, act as if the * company does not exist.</li> * <li>Involved company must exist</li> * <li>Involved company must be visible by the principal user. If not at if it does not exist, one</li> * <li>Involved company must be writable by the principal user when there is one updated attribute. Otherwise * indicate the read-only state.</li> * <li>Involved groups must exist</li> * <li>Involved groups must be visible by the current user, if not, act as if it does not exist. So the user can * only involve visible groups he/she. These groups are completed with the other invisible groups the user may * already have.</li> * <li>Involved changed groups must writable by the principal user. Otherwise indicate the read-only state.</li> * </ul> */ private void validateChanges(final String principal, final UserOrgEditionVo importEntry) { // First cleanup the entry normalize(importEntry); // Get all delegates of current user final List<DelegateOrg> delegates = delegateRepository.findAllByUser(principal); // Get the stored data of the implied user final UserOrg userOrg = getUser().findById(importEntry.getId()); // Check the implied company and request changes final String cleanCompany = Normalizer.normalize(importEntry.getCompany()); final String companyDn = getCompany().findByIdExpected(securityHelper.getLogin(), cleanCompany).getDn(); final boolean hasAttributeChange = hasAttributeChange(importEntry, userOrg); if (hasAttributeChange && !canWrite(delegates, companyDn, DelegateType.COMPANY)) { // Visible but without write access log.info("Attempt to create/update a read-only user '{}', company '{}'", importEntry.getId(), cleanCompany); throw new ValidationJsonException(SimpleUser.COMPANY_ALIAS, READ_ONLY, "0", SimpleUser.COMPANY_ALIAS, "1", importEntry.getCompany()); } // Replace with the normalized company importEntry.setCompany(cleanCompany); // Check the groups : one group not writable implies entry creation to // fail validateAndGroupsCN(userOrg, importEntry, delegates); // Replace the user groups by the normalized groups including the ones // the user does not see if (userOrg != null) { // Check the company change if (!userOrg.getCompany().equals(importEntry.getCompany())) { // Check the user can be removed from the old company checkDeletionRight(importEntry.getId(), "move"); } // Compute merged group identifiers importEntry.setGroups(new ArrayList<>(mergeGroups(delegates, userOrg, importEntry.getGroups()))); } } /** * Validate assigned groups, department and return corresponding group identifiers. * * @param userOrg * The user to update. * @param importEntry * The user raw values to update. * @param delegates * The delegates (read/write) of the principal user. */ private void validateAndGroupsCN(final UserOrg userOrg, final UserOrgEditionVo importEntry, final List<DelegateOrg> delegates) { // First complete the groups with the implicit ones from department final String previous = Optional.ofNullable(userOrg).map(UserOrg::getDepartment).orElse(null); if (ObjectUtils.notEqual(previous, importEntry.getDepartment())) { Optional.ofNullable(toDepartmentGroup(previous)).map(GroupOrg::getId) .ifPresent(importEntry.getGroups()::remove); Optional.ofNullable(toDepartmentGroup(importEntry.getDepartment())).map(GroupOrg::getId) .ifPresent(importEntry.getGroups()::add); } validateAndGroupsCN(Optional.ofNullable(userOrg).map(UserOrg::getGroups).orElse(Collections.emptyList()), importEntry.getGroups(), delegates); } /** * Validate assigned groups, and return corresponding group identifiers. The groups must be visible by the * principal, and added/removed groups from the user must be writable by the principal. * * @param previousGroups * The current user's groups.used to validate the changes. * @param desiredGroups * The groups the principal user has assigned to the user. In this list, there are some read-only groups * previously assigned to this user. Only the changes are checked. * @param delegates * The delegates (read/write) of the principal user. */ private void validateAndGroupsCN(final Collection<String> previousGroups, final Collection<String> desiredGroups, final List<DelegateOrg> delegates) { // Check visibility of the desired groups desiredGroups.forEach(g -> getGroup().findByIdExpected(securityHelper.getLogin(), g)); // Check the visible updated groups can be edited by the principal CollectionUtils.disjunction(desiredGroups, previousGroups).forEach(g -> validateWriteGroup(g, delegates)); } /** * Validate a change of membership of given group by the principal user. * * @param updatedGroup * The group the principal user is updating : add/remove a user. The visibility of this must have been * previously checked. * @param delegates * The delegates (read/write) of the principal user. */ private void validateWriteGroup(final String updatedGroup, final List<DelegateOrg> delegates) { // Check the visible updated groups can be edited by the principal Optional.ofNullable(getGroup().findById(securityHelper.getLogin(), updatedGroup)).filter(Objects::nonNull) .filter(g -> !canWrite(delegates, g.getDn(), DelegateType.GROUP)).ifPresent(g -> { throw new ValidationJsonException(GROUP, READ_ONLY, "0", GROUP, "1", g.getId()); }); } /** * Merge user groups with this formula : * <ul> * <li>DG :Desired groups by current user, and to be set to the entry. These groups must have been previously * checked regarding against the rights the current user has on these groups. So are visible for the principal * user</li> * <li>CG : Current groups of internal entry</li> * <li>VG : Visible groups in CG</li> * <li>WG : Writable groups in VG</li> * <li>GG : Final groups of entry = CG-WG+DG</li> * </ul> * * @param delegates * the available delegates of current principal user. * @param userOrg * The internal user entry to update. * @param groups * The writable groups identifiers to be set to the user in addition of the non visible or writable * groups by the current principal user.. * @return the merged group identifiers to be set internally. */ private Collection<String> mergeGroups(final List<DelegateOrg> delegates, final UserOrg userOrg, final Collection<String> groups) { // Compute the groups merged groups final Collection<String> newGroups = new HashSet<>(userOrg.getGroups()); newGroups.addAll(groups); for (final String oldGroup : userOrg.getGroups()) { final String oldGroupDn = getGroup().findById(oldGroup).getDn(); if (!groups.contains(oldGroup) && canWrite(delegates, oldGroupDn, DelegateType.GROUP)) { // This group is writable, so it has been explicitly removed by // the current user newGroups.remove(oldGroup); } } return newGroups; } /** * Normalize the entry : capitalize and trimming. */ private void normalize(final UserOrgEditionVo importEntry) { // Normalize the identifiers importEntry.setCompany(Normalizer.normalize(importEntry.getCompany())); importEntry.setId(StringUtils.trimToNull(Normalizer.normalize(importEntry.getId()))); importEntry.setGroups(new ArrayList<>(Normalizer.normalize(importEntry.getGroups()))); // Fix the names of user importEntry.setDepartment(StringUtils.trimToNull(importEntry.getDepartment())); importEntry.setLocalId(StringUtils.trimToNull(importEntry.getLocalId())); importEntry.setLastName(WordUtils.capitalizeFully(StringUtils.trimToNull(importEntry.getLastName()))); importEntry.setFirstName(WordUtils.capitalizeFully(StringUtils.trimToNull(importEntry.getFirstName()))); } private boolean canWrite(final List<DelegateOrg> delegates, final String dn, final DelegateType type) { return resource.isAdmin(securityHelper.getLogin()) || delegates.stream().anyMatch(delegate -> canWrite(delegate, dn, type)); } protected boolean canWrite(final DelegateOrg delegate, final String dn, final DelegateType type) { return (delegate.getType() == type || delegate.getType() == DelegateType.TREE) && delegate.isCanWrite() && DnUtils.equalsOrParentOf(delegate.getDn(), dn); } /** * Indicate the two user details have attribute differences */ @SuppressWarnings("unchecked") private boolean hasAttributeChange(final UserOrgEditionVo importEntry, final UserOrg userOrg) { return userOrg == null || hasAttributeChange(importEntry, userOrg, SimpleUser::getFirstName, SimpleUser::getLastName, SimpleUser::getCompany, SimpleUser::getLocalId, SimpleUser::getDepartment) || !userOrg.getMails().contains(importEntry.getMail()); } /** * Indicate the two user details have attribute differences */ private boolean hasAttributeChange(final SimpleUser user1, final SimpleUser user2, @SuppressWarnings("unchecked") final Function<SimpleUser, String>... equals) { return Arrays.stream(equals).anyMatch(f -> !StringUtils.equals(f.apply(user2), f.apply(user1))); } /** * Create the user is not exist and update the related groups and company.<br> * The mail of the entry will replace the one of the repository if it one does not contain any mail. If entry did * not exist or, if there was no password (or a dummy one), it will be set to the one of import of a new generated * password. <br> * When mail or password is updated a mail is sent to the user with the account, and eventually the new * password.<br> * Groups of entry will be normalized. * * @param importEntry * The entry to save or to update. * @param quiet * Flag to turn-off the possible notification such as mail. */ private void saveOrUpdate(final UserOrgEditionVo importEntry, final boolean quiet) { // Create as needed the user, groups will be proceeded after. final IUserRepository repository = getUser(); UserOrg user = repository.findById(importEntry.getId()); final UserOrg newUser = toUserOrg(importEntry); if (user == null) { // Create a new entry in repository log.info("{} will be created", newUser.getId()); user = repository.create(newUser); // Set the password updatePassword(newUser, quiet); } else { updateUser(user, newUser, quiet); } // Update membership repository.updateMembership(importEntry.getGroups(), user); } /** * Create the user is not exist and update the related groups and company.<br> * The mail of the entry will replace the one of the repository if it one does not contain any mail. If entry did * not exist or, if there was no password (or a dummy one), it will be set to the one of import of a new generated * password. <br> * When mail or password is updated a mail is sent to the user with the account, and eventually the new * password.<br> * Groups of entry will be normalized. * * @param importEntry * The entry to save or to update. */ public void saveOrUpdate(final UserOrgEditionVo importEntry) { saveOrUpdate(importEntry, false); } /** * Update the attributes the given user. Groups are not managed there. */ private void updateUser(final UserOrg oldUser, final UserOrg newUser, final boolean quiet) { log.info("{} already exists", newUser.getId()); // First update the DN newUser.setDn(getUser().toDn(newUser)); updateCompanyAsNeeded(oldUser, newUser); // Then, update the no secured attributes : first name, etc. final boolean hadNoMail = oldUser.getMails().isEmpty(); getUser().updateUser(newUser); // Then update the mail and/or password if (newUser.getMails().isEmpty()) { // No mail, no notification log.info("{} already exists, but has no mail", newUser.getId()); } else if (hadNoMail) { // Mail has been added, set a new password log.info("{} already exists, but a mail has been created", newUser.getId()); updatePassword(newUser, quiet); } else if (!oldUser.isSecured()) { // Override the password log.info("{} had no password, a mail will be sent", newUser.getId()); updatePassword(newUser, quiet); } } /** * Convert the import format to the internal format. * * @param importEntry * The raw imported user. * @return The internal format of the user. */ private UserOrg toUserOrg(final UserOrgEditionVo importEntry) { final UserOrg user = new UserOrg(); importEntry.copy(user); user.setGroups(new ArrayList<>()); final List<String> mails = new ArrayList<>(); CollectionUtils.addIgnoreNull(mails, importEntry.getMail()); user.setMails(mails); return user; } /** * Delete an user.<br> * Rules, order is important : * <ul> * <li>Only users managing the company of this user can perform the deletion, if not, act as if the user did not * exist</li> * <li>User must exist</li> * </ul> * Note : even if the user requesting this deletion has no right on the groups the involved user, this operation can * be performed. * * @param user * The user to delete. A normalized form of this parameter will be used for this operation. */ @DELETE @Path("{user}") public void delete(@PathParam("user") final String user) { // Check the user can be deleted final UserOrg userOrg = checkDeletionRight(user, "delete"); // Hard deletion // Check the group : You can't delete an user if he is the last member // of a group final Map<String, GroupOrg> allGroups = getGroup().findAll(); checkLastMemberInGroups(userOrg, allGroups); final IUserRepository repository = getUser(); // Revoke all memberships of this user repository.updateMembership(new ArrayList<>(), userOrg); repository.delete(userOrg); } /** * Disable an user. The user's password is cleared (empty) and a flag is added to tag this user as locked to prevent * further password reset. Other properties are untouched.<br> * Rules, order is important : * <ul> * <li>Only users managing the company of this user can perform the lock, if not, act as if the user did not * exist</li> * <li>User must exist</li> * </ul> * Note : even if the user requesting this operation has no right on the groups of the involved user, this operation * can be performed. * * @param user * The user to lock. A normalized form of this parameter will be used for this operation. */ @DELETE @Path("{user}/lock") public void lock(@PathParam("user") final String user) { getUser().lock(securityHelper.getLogin(), checkDeletionRight(user, "lock")); } /** * Isolate an user. The user is locked and also is moved to a different location from the user repository. This move * ensure some tools to lost this user. Usually the target location is outside the scope/branch of users the other * tools are watching.<br> * All memberships are updated, the user's DN is changed, all groups must be updated. Rules, order is important : * <ul> * <li>Only users managing the company of this user can perform the disable, if not, act as if the user did not * exist</li> * <li>User must exist</li> * </ul> * Note : even if the user requesting this operation has no right on the groups the involved user, this operation * can be performed. * * @param user * The user to move to isolate zone. A normalized form of this parameter will be used for this operation. */ @DELETE @Path("{user}/isolate") public void isolate(@PathParam("user") final String user) { getUser().isolate(securityHelper.getLogin(), checkDeletionRight(user, "isolate")); } /** * Unlock a user.<br> * Rules, order is important : * <ul> * <li>Only users managing the company of this user can perform the enable, if not, act as if the user did not * exist</li> * <li>User must exist</li> * </ul> * Note : even if the user requesting this enable has no right on the groups the involved user, this operation can * be performed. * * @param user * The user to unlock. A normalized form of this parameter will be used for this operation. */ @PUT @Path("{user}/unlock") public void unlock(@PathParam("user") final String user) { getUser().unlock(checkDeletionRight(user, "unlock")); } /** * Restore a user from the isolate zone to the old company.<br> * Rules, order is important : * <ul> * <li>Only users managing the company of this user can perform the enable, if not, act as if the user did not * exist</li> * <li>User must exist</li> * </ul> * Note : even if the user requesting this enable has no right on the groups the involved user, this operation can * be performed. * * @param user * The user to restore. A normalized form of this parameter will be used for this operation. */ @PUT @Path("{user}/restore") public void restore(@PathParam("user") final String user) { getUser().restore(checkDeletionRight(user, "restore")); } /** * Reset a user password, send a mail to him and to the user (principal) requesting this action.<br> * This action is audited when succeed. Rules, order is important : * <ul> * <li>Only users managing the company of this user can perform the operation, if not, act as if the user did not * exist</li> * <li>Target user must exist</li> * <li>Principal user must be an administrator</li> * </ul> * Note: This operation can be performed even if the principal has no right on the groups related to the involved * user. * * @param uid * The user identifier to restore. A normalized form of this parameter will be used for this operation. * @return The generated password. */ @PUT @Path("{user}/reset") @ResponseBody @Produces(MediaType.TEXT_PLAIN) public String resetPassword(@PathParam("user") final String uid) { final UserOrg user = checkResetRight(uid); // Have to generate a new password return Optional.ofNullable(updatePassword(user, false)).map(p -> { // Unlock account if locked getUser().unlock(user); // Log the action logAdminReset(user); return p; }).orElse(null); } /** * Log password reset action triggered by authenticated and privileged user. * * @param user * Target user to log. */ private void logAdminReset(final UserOrg user) { final PasswordResetAudit logReset = new PasswordResetAudit(); logReset.setLogin(user.getId()); passwordResetRepository.saveAndFlush(logReset); } /** * Check the current user can reset the given user password. * * @param user * The user to alter. * @return The internal representation of found user. */ private UserOrg checkResetRight(final String user) { // Check the user exists final UserOrg userOrg = getUser().findByIdExpected(securityHelper.getLogin(), Normalizer.normalize(user)); // Check the company final String companyDn = getCompany().findById(userOrg.getCompany()).getDn(); if (delegateRepository.findByMatchingDnForWrite(securityHelper.getLogin(), companyDn, DelegateType.TREE) .isEmpty()) { // Report this attempt to delete a non writable user log.warn("Attempt to reset the password of a user '{}' out of scope", user); throw new ValidationJsonException(USER_KEY, READ_ONLY, "0", "user", "1", user); } return userOrg; } /** * Check the current user can delete, enable or disable the given user entry. * * @param user * The user to alter. * @param hard * When <code>true</code> the user is completely deleted, in other case, this a simple disable. * @return The internal representation of found user. */ private UserOrg checkDeletionRight(final String user, final String mode) { // Check the user exists final UserOrg userOrg = getUser().findByIdExpected(securityHelper.getLogin(), Normalizer.normalize(user)); // Check the company final String companyDn = getCompany().findById(userOrg.getCompany()).getDn(); if (delegateRepository.findByMatchingDnForWrite(securityHelper.getLogin(), companyDn, DelegateType.COMPANY) .isEmpty()) { // Report this attempt to delete a non writable user log.warn("Attempt to {} a user '{}' out of scope", mode, user); throw new ValidationJsonException(USER_KEY, READ_ONLY, "0", "user", "1", user); } return userOrg; } /** * Check the groups of given users would contain at least another user when it will be deleted. * * @param userOrg * User o delete and to check the memberships. * @param allGroups * Map of group by groupName */ private void checkLastMemberInGroups(final UserOrg userOrg, final Map<String, GroupOrg> allGroups) { for (final String group : userOrg.getGroups()) { if (allGroups.get(group).getMembers().size() == 1) { throw new ValidationJsonException(USER_KEY, "last-member-of-group", "user", userOrg.getId(), GROUP, group); } } } /** * Generate a new password of given user and tag it as secured. The password generation is delegated to the first * password plug-in available. When no plug-in is found, the user is not tagged as secured. * * @param user * The user to update. * @param quiet * Flag to turn-off the possible notification such as mail. * @return The new generated password. When <code>null</code> no password could be generated, and the user is not * tagged as secured. */ protected String updatePassword(final UserOrg user, final boolean quiet) { return applicationContext.getBeansOfType(IPasswordGenerator.class).values().stream().findFirst().map(p -> { // Have to generate a new password final String password = p.generate(user.getId(), quiet); // This user is now secured user.setSecured(true); return password; }).orElse(null); } /** * Return the {@link UserOrg} list corresponding to the given attribute/value without using cache for the search, * but using it for the instances. * * @param attribute * The attribute name to match. * @param value * The attribute value to match. * @return the found users. May be empty. */ public List<UserOrg> findAllBy(final String attribute, final String value) { return getUser().findAllBy(attribute, value); } /** * Return the {@link UserOrg} corresponding to the given attribute/value without using cache. * * @param user * The user to find. A normalized form will be used for the search. * @return the found user or <code>null</code> when not found. Groups are not fetched for this operation. */ public UserOrg findByIdNoCache(final String user) { return getUser().findByIdNoCache(Normalizer.normalize(user)); } /** * Update internal user with the new user. Note the security is not checked there. * * @param userOrg * The internal user to update. Note this must be the internal instance * @param newUser * The new user data. Note this will not be the stored instance. */ private void updateCompanyAsNeeded(final UserOrg userOrg, final UserOrg newUser) { // Check the company if (ObjectUtils.notEqual(userOrg.getCompany(), newUser.getCompany())) { // Move the user getUser().move(userOrg, getCompany().findById(newUser.getCompany())); } } /** * Return the group corresponding to the given department. * * @param department * The department to match. * @return The group corresponding to the given department or <code>null</code>. */ private GroupOrg toDepartmentGroup(final String department) { return Optional.ofNullable(department).map(getGroup()::findByDepartment).orElse(null); } /** * Update internal user with the new user for following attributes : department and local identifier. Note the * security is not checked there. * * @param userOrg * The user to update. Note this must be the internal instance. * @param newUser * The new user data. Note this will not be the stored instance. */ public void mergeUser(final UserOrg userOrg, final UserOrg newUser) { boolean needUpdate = false; // Merge department if (ObjectUtils.notEqual(userOrg.getDepartment(), newUser.getDepartment())) { // Remove membership from the old department if exist Optional.ofNullable(toDepartmentGroup(userOrg.getDepartment())) .ifPresent(g -> getGroup().removeUser(userOrg, g.getId())); // Add membership to the new department if exist Optional.ofNullable(toDepartmentGroup(newUser.getDepartment())) .ifPresent(g -> getGroup().addUser(userOrg, g.getId())); userOrg.setDepartment(newUser.getDepartment()); needUpdate = true; } // Merge local identifier if (ObjectUtils.notEqual(userOrg.getLocalId(), newUser.getLocalId())) { userOrg.setLocalId(newUser.getLocalId()); } // Updated as needed if (needUpdate) { getUser().updateUser(userOrg); } } }