View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.user2;
20  
21  import static org.mycore.user2.utils.MCRUserTransformer.JAXB_CONTEXT;
22  
23  import java.io.IOException;
24  import java.net.URLEncoder;
25  import java.nio.charset.StandardCharsets;
26  import java.text.DateFormat;
27  import java.text.ParseException;
28  import java.text.SimpleDateFormat;
29  import java.util.Collections;
30  import java.util.Date;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Optional;
34  import java.util.Set;
35  import java.util.TimeZone;
36  import java.util.stream.Collectors;
37  
38  import org.apache.logging.log4j.LogManager;
39  import org.apache.logging.log4j.Logger;
40  import org.jdom2.Attribute;
41  import org.jdom2.Document;
42  import org.jdom2.Element;
43  import org.jdom2.filter.Filters;
44  import org.jdom2.xpath.XPathExpression;
45  import org.jdom2.xpath.XPathFactory;
46  import org.mycore.access.MCRAccessManager;
47  import org.mycore.common.MCRSessionMgr;
48  import org.mycore.common.MCRSystemUserInformation;
49  import org.mycore.common.config.MCRConfiguration2;
50  import org.mycore.common.content.MCRJAXBContent;
51  import org.mycore.common.content.MCRJDOMContent;
52  import org.mycore.datamodel.common.MCRISO8601Date;
53  import org.mycore.frontend.servlets.MCRServlet;
54  import org.mycore.frontend.servlets.MCRServletJob;
55  import org.mycore.services.i18n.MCRTranslation;
56  import org.mycore.user2.utils.MCRUserTransformer;
57  
58  import jakarta.servlet.http.HttpServletRequest;
59  import jakarta.servlet.http.HttpServletResponse;
60  
61  /**
62   * Provides functionality to search for users, list users, 
63   * retrieve, delete or update user data. 
64   * 
65   * @author Frank L\u00fctzenkirchen
66   * @author Thomas Scheffler (yagee)
67   */
68  public class MCRUserServlet extends MCRServlet {
69      private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC");
70  
71      private static final long serialVersionUID = 1L;
72  
73      /** The logger */
74      private static final Logger LOGGER = LogManager.getLogger(MCRUserServlet.class);
75  
76      /**
77       * Handles requests. The parameter 'action' selects what to do, possible
78       * values are show, save, delete, password (with id as second parameter). 
79       * The default is to search and list users. 
80       */
81      public void doGetPost(MCRServletJob job) throws Exception {
82          HttpServletRequest req = job.getRequest();
83          HttpServletResponse res = job.getResponse();
84          if (forbidIfGuest(res)) {
85              return;
86          }
87          String action = req.getParameter("action");
88          String uid = req.getParameter("id");
89          MCRUser user;
90  
91          if ((uid == null) || (uid.trim().length() == 0)) {
92              user = MCRUserManager.getCurrentUser();
93              uid = user != null ? String.valueOf(user.getUserID()) : null;
94              if (!(user instanceof MCRTransientUser)) {
95                  //even reload current user, so that owner is correctly initialized
96                  user = MCRUserManager.getUser(uid);
97              }
98          } else {
99              user = MCRUserManager.getUser(uid);
100         }
101 
102         if ("show".equals(action)) {
103             showUser(req, res, user, uid);
104         } else if ("save".equals(action)) {
105             saveUser(req, res);
106         } else if ("saveCurrentUser".equals(action)) {
107             saveCurrentUser(req, res);
108         } else if ("changeMyPassword".equals(action)) {
109             redirectToPasswordChangePage(req, res);
110         } else if ("password".equals(action)) {
111             changePassword(req, res, user, uid);
112         } else if ("delete".equals(action)) {
113             deleteUser(req, res, user);
114         } else {
115             listUsers(req, res);
116         }
117     }
118 
119     private void redirectToPasswordChangePage(HttpServletRequest req, HttpServletResponse res) throws Exception {
120         MCRUser currentUser = MCRUserManager.getCurrentUser();
121         if (!checkUserIsNotNull(res, currentUser, null)) {
122             return;
123         }
124         if (checkUserIsLocked(res, currentUser) || checkUserIsDisabled(res, currentUser)) {
125             return;
126         }
127         String url = currentUser.getRealm().getPasswordChangeURL();
128         if (url == null) {
129             String msg = MCRTranslation.translate("component.user2.UserServlet.missingRealPasswortChangeURL",
130                 currentUser.getRealmID());
131             res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg);
132         } else {
133             res.sendRedirect(url);
134         }
135     }
136 
137     private static boolean checkUserIsNotNull(HttpServletResponse res, MCRUser currentUser, String userID)
138         throws IOException {
139         if (currentUser == null) {
140             String uid = userID == null ? MCRSessionMgr.getCurrentSession().getUserInformation().getUserID() : userID;
141             String msg = MCRTranslation.translate("component.user2.UserServlet.currentUserUnknown", uid);
142             res.sendError(HttpServletResponse.SC_FORBIDDEN, msg);
143             return false;
144         }
145         return true;
146     }
147 
148     private static boolean checkUserIsLocked(HttpServletResponse res, MCRUser currentUser) throws IOException {
149         if (currentUser.isLocked()) {
150             String userName = currentUser.getUserID();
151             String msg = MCRTranslation.translate("component.user2.UserServlet.isLocked", userName);
152             res.sendError(HttpServletResponse.SC_FORBIDDEN, msg);
153             return true;
154         }
155         return false;
156     }
157 
158     private static boolean checkUserIsDisabled(HttpServletResponse res, MCRUser currentUser) throws IOException {
159         if (currentUser.isDisabled()) {
160             String userName = currentUser.getUserID();
161             String msg = MCRTranslation.translate("component.user2.UserServlet.isDisabled", userName);
162             res.sendError(HttpServletResponse.SC_FORBIDDEN, msg);
163             return true;
164         }
165         return false;
166     }
167 
168     private static boolean forbidIfGuest(HttpServletResponse res) throws IOException {
169         if (MCRSessionMgr.getCurrentSession().getUserInformation().getUserID()
170             .equals(MCRSystemUserInformation.getGuestInstance().getUserID())) {
171             String msg = MCRTranslation.translate("component.user2.UserServlet.noGuestAction");
172             res.sendError(HttpServletResponse.SC_FORBIDDEN, msg);
173             return true;
174         }
175         return false;
176     }
177 
178     /**
179      * Handles MCRUserServlet?action=show&id={userID}.
180      * Outputs user data for the given id using user.xsl.
181      */
182     private void showUser(HttpServletRequest req, HttpServletResponse res, MCRUser user, String uid) throws Exception {
183         MCRUser currentUser = MCRUserManager.getCurrentUser();
184         if (!checkUserIsNotNull(res, currentUser, null) || !checkUserIsNotNull(res, user, uid)) {
185             return;
186         }
187         boolean allowed = MCRAccessManager.checkPermission(MCRUser2Constants.USER_ADMIN_PERMISSION)
188             || currentUser.equals(user) || currentUser.equals(user.getOwner());
189         if (!allowed) {
190             String msg = MCRTranslation.translate("component.user2.UserServlet.noAdminPermission");
191             res.sendError(HttpServletResponse.SC_FORBIDDEN, msg);
192             return;
193         }
194 
195         LOGGER.info("show user {} {} {}", user.getUserID(), user.getUserName(), user.getRealmID());
196         getLayoutService().doLayout(req, res, getContent(user));
197     }
198 
199     /**
200      * Invoked by editor form user-editor.xed to check for a valid
201      * login user name.
202      */
203     public static boolean checkUserName(String userName) {
204         String realmID = MCRRealmFactory.getLocalRealm().getID();
205 
206         // Check for required fields is done in the editor form itself, not here
207         if ((userName == null) || (realmID == null)) {
208             return true;
209         }
210 
211         // In all other cases, combination of userName and realm must not exist
212         return !MCRUserManager.exists(userName, realmID);
213     }
214 
215     private void saveCurrentUser(HttpServletRequest req, HttpServletResponse res) throws IOException {
216         MCRUser currentUser = MCRUserManager.getCurrentUser();
217         if (!checkUserIsNotNull(res, currentUser, null)) {
218             return;
219         }
220         if (checkUserIsLocked(res, currentUser) || checkUserIsDisabled(res, currentUser)) {
221             return;
222         }
223         if (!currentUser.hasNoOwner() && currentUser.isLocked()) {
224             res.sendError(HttpServletResponse.SC_FORBIDDEN);
225             return;
226         }
227 
228         Document doc = (Document) (req.getAttribute("MCRXEditorSubmission"));
229         Element u = doc.getRootElement();
230         updateBasicUserInfo(u, currentUser);
231         MCRUserManager.updateUser(currentUser);
232 
233         res.sendRedirect(res.encodeRedirectURL("MCRUserServlet?action=show"));
234     }
235 
236     /**
237      * Handles MCRUserServlet?action=save&id={userID}.
238      * This is called by user-editor.xml editor form to save the
239      * changed user data from editor submission. Redirects to
240      * show user data afterwards. 
241      */
242     private void saveUser(HttpServletRequest req, HttpServletResponse res) throws Exception {
243         MCRUser currentUser = MCRUserManager.getCurrentUser();
244         if (!checkUserIsNotNull(res, currentUser, null)) {
245             return;
246         }
247         boolean hasAdminPermission = MCRAccessManager.checkPermission(MCRUser2Constants.USER_ADMIN_PERMISSION);
248         boolean allowed = hasAdminPermission
249             || MCRAccessManager.checkPermission(MCRUser2Constants.USER_CREATE_PERMISSION);
250         if (!allowed) {
251             String msg = MCRTranslation.translate("component.user2.UserServlet.noCreatePermission");
252             res.sendError(HttpServletResponse.SC_FORBIDDEN, msg);
253             return;
254         }
255 
256         Document doc = (Document) (req.getAttribute("MCRXEditorSubmission"));
257         Element u = doc.getRootElement();
258         String userName = u.getAttributeValue("name");
259 
260         String realmID = MCRRealmFactory.getLocalRealm().getID();
261         if (hasAdminPermission) {
262             realmID = u.getAttributeValue("realm");
263         }
264 
265         MCRUser user;
266         boolean userExists = MCRUserManager.exists(userName, realmID);
267         if (!userExists) {
268             user = new MCRUser(userName, realmID);
269             LOGGER.info("create new user {} {}", userName, realmID);
270 
271             // For new local users, set password
272             String pwd = u.getChildText("password");
273             if ((pwd != null) && (pwd.trim().length() > 0) && user.getRealm().equals(MCRRealmFactory.getLocalRealm())) {
274                 MCRUserManager.updatePasswordHashToSHA256(user, pwd);
275             }
276         } else {
277             user = MCRUserManager.getUser(userName, realmID);
278             if (!(hasAdminPermission || currentUser.equals(user) || currentUser.equals(user.getOwner()))) {
279                 res.sendError(HttpServletResponse.SC_FORBIDDEN);
280                 return;
281             }
282         }
283 
284         XPathExpression<Attribute> hintPath = XPathFactory.instance().compile("password/@hint", Filters.attribute());
285         Attribute hintAttr = hintPath.evaluateFirst(u);
286         String hint = hintAttr == null ? null : hintAttr.getValue();
287         if ((hint != null) && (hint.trim().length() == 0)) {
288             hint = null;
289         }
290         user.setHint(hint);
291 
292         updateBasicUserInfo(u, user);
293 
294         if (hasAdminPermission) {
295             boolean locked = "true".equals(u.getAttributeValue("locked"));
296             user.setLocked(locked);
297 
298             boolean disabled = "true".equals(u.getAttributeValue("disabled"));
299             user.setDisabled(disabled);
300 
301             Element o = u.getChild("owner");
302             if (o != null && !o.getAttributes().isEmpty()) {
303                 String ownerName = o.getAttributeValue("name");
304                 String ownerRealm = o.getAttributeValue("realm");
305                 MCRUser owner = MCRUserManager.getUser(ownerName, ownerRealm);
306                 if (!checkUserIsNotNull(res, owner, ownerName + "@" + ownerRealm)) {
307                     return;
308                 }
309                 user.setOwner(owner);
310             } else {
311                 user.setOwner(null);
312             }
313             String validUntilText = u.getChildTextTrim("validUntil");
314             if (validUntilText == null || validUntilText.length() == 0) {
315                 user.setValidUntil(null);
316             } else {
317 
318                 String dateInUTC = validUntilText;
319                 if (validUntilText.length() == 10) {
320                     dateInUTC = convertToUTC(validUntilText, "yyyy-MM-dd");
321                 }
322 
323                 MCRISO8601Date date = new MCRISO8601Date(dateInUTC);
324                 user.setValidUntil(date.getDate());
325             }
326         } else { // save read user of creator
327             user.setRealm(MCRRealmFactory.getLocalRealm());
328             user.setOwner(currentUser);
329         }
330         Element gs = u.getChild("roles");
331         if (gs != null) {
332             user.getSystemRoleIDs().clear();
333             user.getExternalRoleIDs().clear();
334             List<Element> groupList = gs.getChildren("role");
335             for (Element group : groupList) {
336                 String groupName = group.getAttributeValue("name");
337                 if (hasAdminPermission || currentUser.isUserInRole(groupName)) {
338                     user.assignRole(groupName);
339                 } else {
340                     LOGGER.warn("Current user {} has not the permission to add user to group {}",
341                         currentUser.getUserID(), groupName);
342                 }
343             }
344         }
345 
346         if (userExists) {
347             MCRUserManager.updateUser(user);
348         } else {
349             MCRUserManager.createUser(user);
350         }
351 
352         res.sendRedirect(res.encodeRedirectURL("MCRUserServlet?action=show&id="
353             + URLEncoder.encode(user.getUserID(), StandardCharsets.UTF_8)));
354     }
355 
356     private String convertToUTC(String validUntilText, String format) throws ParseException {
357         DateFormat inputFormat = new SimpleDateFormat(format, Locale.ROOT);
358         inputFormat.setTimeZone(UTC_TIME_ZONE);
359         DateFormat outputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX", Locale.ROOT);
360 
361         Date d = inputFormat.parse(validUntilText);
362         outputFormat.setTimeZone(UTC_TIME_ZONE);
363         return outputFormat.format(d);
364     }
365 
366     private void updateBasicUserInfo(Element u, MCRUser user) {
367         String name = u.getChildText("realName");
368         if ((name != null) && (name.trim().length() == 0)) {
369             name = null;
370         }
371         user.setRealName(name);
372 
373         String eMail = u.getChildText("eMail");
374         if ((eMail != null) && (eMail.trim().length() == 0)) {
375             eMail = null;
376         }
377         user.setEMail(eMail);
378 
379         List<Element> attributeList = Optional.ofNullable(u.getChild("attributes"))
380             .map(attributes -> attributes.getChildren("attribute"))
381             .orElse(Collections.emptyList());
382         Set<MCRUserAttribute> newAttrs = attributeList.stream()
383             .map(a -> new MCRUserAttribute(a.getAttributeValue("name"), a.getAttributeValue("value")))
384             .collect(Collectors.toSet());
385         user.getAttributes().retainAll(newAttrs);
386         newAttrs.removeAll(user.getAttributes());
387         user.getAttributes().addAll(newAttrs);
388     }
389 
390     /**
391      * Handles MCRUserServlet?action=save&id={userID}.
392      * This is called by user-editor.xml editor form to save the
393      * changed user data from editor submission. Redirects to
394      * show user data afterwards. 
395      */
396     private void changePassword(HttpServletRequest req, HttpServletResponse res, MCRUser user, String uid)
397         throws Exception {
398         MCRUser currentUser = MCRUserManager.getCurrentUser();
399         if (!checkUserIsNotNull(res, currentUser, null) || !checkUserIsNotNull(res, user, uid)) {
400             return;
401         }
402         boolean allowed = MCRAccessManager.checkPermission(MCRUser2Constants.USER_ADMIN_PERMISSION)
403             || currentUser.equals(user.getOwner())
404             || currentUser.equals(user) && !currentUser.isLocked();
405         if (!allowed) {
406             String msg = MCRTranslation.translate("component.user2.UserServlet.noAdminPermission");
407             res.sendError(HttpServletResponse.SC_FORBIDDEN, msg);
408             return;
409         }
410 
411         LOGGER.info("change password of user {} {} {}", user.getUserID(), user.getUserName(), user.getRealmID());
412 
413         Document doc = (Document) (req.getAttribute("MCRXEditorSubmission"));
414         String password = doc.getRootElement().getChildText("password");
415         MCRUserManager.setPassword(user, password);
416 
417         res.sendRedirect(res.encodeRedirectURL("MCRUserServlet?action=show&XSL.step=changedPassword&id="
418             + URLEncoder.encode(user.getUserID(), StandardCharsets.UTF_8)));
419     }
420 
421     /**
422      * Handles MCRUserServlet?action=delete&id={userID}.
423      * Deletes the user. 
424      * Outputs user data of the deleted user using user.xsl afterwards.
425      */
426     private void deleteUser(HttpServletRequest req, HttpServletResponse res, MCRUser user) throws Exception {
427         MCRUser currentUser = MCRUserManager.getCurrentUser();
428         boolean allowed = MCRAccessManager.checkPermission(MCRUser2Constants.USER_ADMIN_PERMISSION)
429             || currentUser.equals(user.getOwner());
430         if (!allowed) {
431             String msg = MCRTranslation.translate("component.user2.UserServlet.noAdminPermission");
432             res.sendError(HttpServletResponse.SC_FORBIDDEN, msg);
433             return;
434         }
435 
436         LOGGER.info("delete user {} {} {}", user.getUserID(), user.getUserName(), user.getRealmID());
437         MCRUserManager.deleteUser(user);
438         getLayoutService().doLayout(req, res, getContent(user));
439     }
440 
441     private MCRJAXBContent<MCRUser> getContent(MCRUser user) {
442         return new MCRJAXBContent<>(JAXB_CONTEXT, user.getSafeCopy());
443     }
444 
445     /**
446      * Handles MCRUserServlet?search={pattern}, which is an optional parameter.
447      * Searches for users matching the pattern in user name or real name and outputs
448      * the list of results using users.xsl. The search pattern may contain * and ?
449      * wildcard characters. The property MCR.user2.Users.MaxResults (default 100) specifies
450      * the maximum number of users to return. When there are more hits, just the
451      * number of results is returned.
452      * 
453      * When current user is not admin, the search pattern will be ignored and only all 
454      * the users the current user is owner of will be listed.
455      */
456     private void listUsers(HttpServletRequest req, HttpServletResponse res) throws Exception {
457         MCRUser currentUser = MCRUserManager.getCurrentUser();
458         List<MCRUser> ownUsers = MCRUserManager.listUsers(currentUser);
459         boolean hasAdminPermission = MCRAccessManager.checkPermission(MCRUser2Constants.USER_ADMIN_PERMISSION);
460         boolean allowed = hasAdminPermission
461             || MCRAccessManager.checkPermission(MCRUser2Constants.USER_CREATE_PERMISSION) || !ownUsers.isEmpty();
462         if (!allowed) {
463             String msg = MCRTranslation.translate("component.user2.UserServlet.noCreatePermission");
464             res.sendError(HttpServletResponse.SC_FORBIDDEN, msg);
465             return;
466         }
467 
468         Element users = new Element("users");
469 
470         List<MCRUser> results = null;
471         if (hasAdminPermission) {
472             String search = req.getParameter("search");
473             if ((search == null) || search.trim().length() == 0) {
474                 search = null;
475             }
476 
477             if (search != null) {
478                 users.setAttribute("search", search);
479                 search = "*" + search + "*";
480             }
481 
482             LOGGER.info("search users like {}", search);
483 
484             int max = MCRConfiguration2.getInt(MCRUser2Constants.CONFIG_PREFIX + "Users.MaxResults").orElse(100);
485             int num = MCRUserManager.countUsers(search, null, search, search);
486 
487             if ((num < max) && (num > 0)) {
488                 results = MCRUserManager.listUsers(search, null, search, search);
489             }
490             users.setAttribute("num", String.valueOf(num));
491             users.setAttribute("max", String.valueOf(max));
492         } else {
493             LOGGER.info("list owned users of {} {}", currentUser.getUserName(), currentUser.getRealmID());
494             results = ownUsers;
495         }
496 
497         if (results != null) {
498             for (MCRUser user : results) {
499                 Element u = MCRUserTransformer.buildBasicXML(user).detachRootElement();
500                 addString(u, "realName", user.getRealName());
501                 addString(u, "eMail", user.getEMailAddress());
502                 users.addContent(u);
503             }
504         }
505 
506         getLayoutService().doLayout(req, res, new MCRJDOMContent(users));
507     }
508 
509     private void addString(Element parent, String name, String value) {
510         if ((value != null) && (value.trim().length() > 0)) {
511             parent.addContent(new Element(name).setText(value.trim()));
512         }
513     }
514 }