View Javadoc
1   /*
2    * Copyright 2012-2021 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.app.web.admin.user;
17  
18  import java.util.Base64;
19  import java.util.HashMap;
20  import java.util.Map;
21  import java.util.function.Consumer;
22  
23  import javax.annotation.Resource;
24  
25  import org.apache.commons.lang3.ArrayUtils;
26  import org.apache.logging.log4j.LogManager;
27  import org.apache.logging.log4j.Logger;
28  import org.codelibs.core.lang.StringUtil;
29  import org.codelibs.fess.Constants;
30  import org.codelibs.fess.annotation.Secured;
31  import org.codelibs.fess.app.pager.UserPager;
32  import org.codelibs.fess.app.service.GroupService;
33  import org.codelibs.fess.app.service.RoleService;
34  import org.codelibs.fess.app.service.UserService;
35  import org.codelibs.fess.app.web.CrudMode;
36  import org.codelibs.fess.app.web.base.FessAdminAction;
37  import org.codelibs.fess.app.web.base.login.FessLoginAssist;
38  import org.codelibs.fess.es.user.exentity.User;
39  import org.codelibs.fess.mylasta.action.FessMessages;
40  import org.codelibs.fess.util.ComponentUtil;
41  import org.codelibs.fess.util.RenderDataUtil;
42  import org.dbflute.optional.OptionalEntity;
43  import org.dbflute.optional.OptionalThing;
44  import org.lastaflute.web.Execute;
45  import org.lastaflute.web.response.HtmlResponse;
46  import org.lastaflute.web.response.render.RenderData;
47  import org.lastaflute.web.ruts.process.ActionRuntime;
48  import org.lastaflute.web.validation.VaErrorHook;
49  import org.lastaflute.web.validation.VaMessenger;
50  
51  /**
52   * @author shinsuke
53   * @author Keiichi Watanabe
54   */
55  public class AdminUserAction extends FessAdminAction {
56  
57      public static final String ROLE = "admin-user";
58  
59      private static final Logger logger = LogManager.getLogger(AdminUserAction.class);
60  
61      // ===================================================================================
62      //                                                                           Attribute
63      //                                                                           =========
64      @Resource
65      private UserService userService;
66      @Resource
67      private RoleService roleService;
68      @Resource
69      private GroupService groupService;
70      @Resource
71      private UserPager userPager;
72  
73      // ===================================================================================
74      //                                                                               Hook
75      //                                                                              ======
76      @Override
77      protected void setupHtmlData(final ActionRuntime runtime) {
78          super.setupHtmlData(runtime);
79          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameUser()));
80          runtime.registerData("ldapAdminEnabled", fessConfig.isLdapAdminEnabled());
81      }
82  
83      @Override
84      protected String getActionRole() {
85          return ROLE;
86      }
87  
88      // ===================================================================================
89      //                                                                      Search Execute
90      //                                                                      ==============
91      @Execute
92      @Secured({ ROLE, ROLE + VIEW })
93      public HtmlResponse index() {
94          return asListHtml();
95      }
96  
97      @Execute
98      @Secured({ ROLE, ROLE + VIEW })
99      public HtmlResponse list(final OptionalThing<Integer> pageNumber, final SearchForm form) {
100         pageNumber.ifPresent(num -> {
101             userPager.setCurrentPageNumber(pageNumber.get());
102         }).orElse(() -> {
103             userPager.setCurrentPageNumber(0);
104         });
105         return asHtml(path_AdminUser_AdminUserJsp).renderWith(data -> {
106             searchPaging(data, form);
107         });
108     }
109 
110     @Execute
111     @Secured({ ROLE, ROLE + VIEW })
112     public HtmlResponse search(final SearchForm form) {
113         copyBeanToBean(form, userPager, op -> op.exclude(Constants.PAGER_CONVERSION_RULE));
114         return asHtml(path_AdminUser_AdminUserJsp).renderWith(data -> {
115             searchPaging(data, form);
116         });
117     }
118 
119     @Execute
120     @Secured({ ROLE, ROLE + VIEW })
121     public HtmlResponse reset(final SearchForm form) {
122         userPager.clear();
123         return asHtml(path_AdminUser_AdminUserJsp).renderWith(data -> {
124             searchPaging(data, form);
125         });
126     }
127 
128     protected void searchPaging(final RenderData data, final SearchForm form) {
129         RenderDataUtil.register(data, "userItems", userService.getUserList(userPager)); // page navi
130         // restore from pager
131         copyBeanToBean(userPager, form, op -> op.include("id"));
132     }
133 
134     private void registerForms(final RenderData data) {
135         RenderDataUtil.register(data, "roleItems", roleService.getAvailableRoleList());
136         RenderDataUtil.register(data, "groupItems", groupService.getAvailableGroupList());
137     }
138 
139     // ===================================================================================
140     //                                                                        Edit Execute
141     //                                                                        ============
142     // -----------------------------------------------------
143     //                                            Entry Page
144     //                                            ----------
145     @Execute
146     @Secured({ ROLE })
147     public HtmlResponse createnew() {
148         saveToken();
149         return asHtml(path_AdminUser_AdminUserEditJsp).useForm(CreateForm.class, op -> {
150             op.setup(form -> {
151                 form.initialize();
152                 form.crudMode = CrudMode.CREATE;
153             });
154         }).renderWith(data -> {
155             registerForms(data);
156         });
157     }
158 
159     @Execute
160     @Secured({ ROLE })
161     public HtmlResponse edit(final EditForm form) {
162         validate(form, messages -> {}, this::asListHtml);
163         final String id = form.id;
164         userService.getUser(id).ifPresent(entity -> {
165             copyBeanToBean(entity, form, op -> {});
166         }).orElse(() -> {
167             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml);
168         });
169         resetPassword(form);
170         saveToken();
171         if (form.crudMode.intValue() == CrudMode.EDIT) {
172             // back
173             form.crudMode = CrudMode.DETAILS;
174             return asDetailsHtml();
175         }
176         form.crudMode = CrudMode.EDIT;
177         return asEditHtml();
178     }
179 
180     // -----------------------------------------------------
181     //                                               Details
182     //                                               -------
183     @Execute
184     @Secured({ ROLE, ROLE + VIEW })
185     public HtmlResponse details(final int crudMode, final String id) {
186         verifyCrudMode(crudMode, CrudMode.DETAILS);
187         saveToken();
188         return asHtml(path_AdminUser_AdminUserDetailsJsp).useForm(EditForm.class, op -> {
189             op.setup(form -> {
190                 userService.getUser(id).ifPresent(entity -> {
191                     copyBeanToBean(entity, form, copyOp -> {
192                         copyOp.excludeNull();
193                     });
194                     form.crudMode = crudMode;
195                 }).orElse(() -> {
196                     throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml);
197                 });
198                 resetPassword(form);
199             });
200         }).renderWith(data -> {
201             registerForms(data);
202         });
203     }
204 
205     // -----------------------------------------------------
206     //                                         Actually Crud
207     //                                         -------------
208     @Execute
209     @Secured({ ROLE })
210     public HtmlResponse create(final CreateForm form) {
211         verifyCrudMode(form.crudMode, CrudMode.CREATE);
212         validate(form, messages -> {}, this::asEditHtml);
213         validateAttributes(form.attributes, v -> throwValidationError(v, this::asEditHtml));
214         verifyPassword(form, this::asEditHtml);
215         verifyToken(this::asEditHtml);
216         getUser(form).ifPresent(entity -> {
217             try {
218                 userService.store(entity);
219                 saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
220             } catch (final Exception e) {
221                 logger.error("Failed to add {}", entity, e);
222                 throwValidationError(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)),
223                         this::asEditHtml);
224             }
225         }).orElse(() -> {
226             throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), this::asEditHtml);
227         });
228         return redirect(getClass());
229     }
230 
231     @Execute
232     @Secured({ ROLE })
233     public HtmlResponse update(final EditForm form) {
234         verifyCrudMode(form.crudMode, CrudMode.EDIT);
235         validate(form, messages -> {}, this::asEditHtml);
236         validateAttributes(form.attributes, v -> throwValidationError(v, this::asEditHtml));
237         verifyPassword(form, this::asEditHtml);
238         verifyToken(this::asEditHtml);
239         getUser(form).ifPresent(entity -> {
240             try {
241                 userService.store(entity);
242                 saveInfo(messages -> messages.addSuccessCrudUpdateCrudTable(GLOBAL));
243             } catch (final Exception e) {
244                 logger.error("Failed to update {}", entity, e);
245                 throwValidationError(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)),
246                         this::asEditHtml);
247             }
248         }).orElse(() -> {
249             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.id), this::asEditHtml);
250         });
251         return redirect(getClass());
252     }
253 
254     @Execute
255     @Secured({ ROLE })
256     public HtmlResponse delete(final EditForm form) {
257         verifyCrudMode(form.crudMode, CrudMode.DETAILS);
258         validate(form, messages -> {}, this::asDetailsHtml);
259         getUserBean().ifPresent(u -> {
260             if (u.getFessUser() instanceof User && form.name.equals(u.getUserId())) {
261                 throwValidationError(messages -> messages.addErrorsCouldNotDeleteLoggedInUser(GLOBAL), this::asDetailsHtml);
262             }
263         });
264         verifyToken(this::asDetailsHtml);
265         final String id = form.id;
266         userService.getUser(id).ifPresent(entity -> {
267             try {
268                 userService.delete(entity);
269                 saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
270             } catch (final Exception e) {
271                 logger.error("Failed to delete {}", entity, e);
272                 throwValidationError(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)),
273                         this::asDetailsHtml);
274             }
275         }).orElse(() -> {
276             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asDetailsHtml);
277         });
278         return redirect(getClass());
279     }
280 
281     //===================================================================================
282     //                                                                       Assist Logic
283     //                                                                       ============
284     private static OptionalEntity<User> getEntity(final CreateForm form) {
285         switch (form.crudMode) {
286         case CrudMode.CREATE:
287             return OptionalEntity.of(new User()).map(entity -> {
288                 entity.setId(Base64.getUrlEncoder().encodeToString(form.name.getBytes(Constants.CHARSET_UTF_8)));
289                 return entity;
290             });
291         case CrudMode.EDIT:
292             if (form instanceof EditForm) {
293                 return ComponentUtil.getComponent(UserService.class).getUser(((EditForm) form).id);
294             }
295             break;
296         default:
297             break;
298         }
299         return OptionalEntity.empty();
300     }
301 
302     public static OptionalEntity<User> getUser(final CreateForm form) {
303         return getEntity(form).map(entity -> {
304             copyMapToBean(form.attributes, entity, op -> op.exclude(Constants.COMMON_CONVERSION_RULE));
305             copyBeanToBean(form, entity, op -> op.exclude(ArrayUtils.addAll(Constants.COMMON_CONVERSION_RULE, "password")));
306             if (form.crudMode.intValue() == CrudMode.CREATE || StringUtil.isNotBlank(form.password)) {
307                 final String encodedPassword = ComponentUtil.getComponent(FessLoginAssist.class).encryptPassword(form.password);
308                 entity.setOriginalPassword(form.password);
309                 entity.setPassword(encodedPassword);
310             }
311             return entity;
312         });
313     }
314 
315     protected Map<String, String> createItem(final String label, final String value) {
316         final Map<String, String> map = new HashMap<>(2);
317         map.put(Constants.ITEM_LABEL, label);
318         map.put(Constants.ITEM_VALUE, value);
319         return map;
320     }
321 
322     // ===================================================================================
323     //                                                                        Small Helper
324     //                                                                        ============
325 
326     protected void verifyCrudMode(final int crudMode, final int expectedMode) {
327         if (crudMode != expectedMode) {
328             throwValidationError(messages -> {
329                 messages.addErrorsCrudInvalidMode(GLOBAL, String.valueOf(expectedMode), String.valueOf(crudMode));
330             }, this::asListHtml);
331         }
332     }
333 
334     protected void verifyPassword(final CreateForm form, final VaErrorHook validationErrorLambda) {
335         if (form.crudMode == CrudMode.CREATE && StringUtil.isBlank(form.password)) {
336             resetPassword(form);
337             throwValidationError(messages -> {
338                 messages.addErrorsBlankPassword("password");
339             }, validationErrorLambda);
340         }
341         if (form.password != null && !form.password.equals(form.confirmPassword)) {
342             form.confirmPassword = null;
343             throwValidationError(messages -> {
344                 messages.addErrorsInvalidConfirmPassword("confirmPassword");
345             }, validationErrorLambda);
346         }
347     }
348 
349     public static void resetPassword(final CreateForm form) {
350         form.password = null;
351         form.confirmPassword = null;
352     }
353 
354     public static void validateAttributes(final Map<String, String> attributes, final Consumer<VaMessenger<FessMessages>> throwError) {
355         ComponentUtil.getLdapManager().validateUserAttributes(Long.class, attributes,
356                 s -> throwError.accept(messages -> messages.addErrorsPropertyTypeLong("attributes." + s, "attributes." + s)));
357     }
358 
359     // ===================================================================================
360     //                                                                              JSP
361     //                                                                           =========
362 
363     private HtmlResponse asListHtml() {
364         return asHtml(path_AdminUser_AdminUserJsp).renderWith(data -> {
365             RenderDataUtil.register(data, "userItems", userService.getUserList(userPager)); // page navi
366         }).useForm(SearchForm.class, setup -> {
367             setup.setup(form -> {
368                 copyBeanToBean(userPager, form, op -> op.include("id"));
369             });
370         });
371     }
372 
373     private HtmlResponse asEditHtml() {
374         return asHtml(path_AdminUser_AdminUserEditJsp).renderWith(data -> {
375             registerForms(data);
376         });
377     }
378 
379     private HtmlResponse asDetailsHtml() {
380         return asHtml(path_AdminUser_AdminUserDetailsJsp).renderWith(data -> {
381             registerForms(data);
382         });
383     }
384 }