View Javadoc
1   /*
2    * Copyright 2012-2025 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.helper;
17  
18  import static org.codelibs.core.stream.StreamUtil.stream;
19  
20  import java.time.ZonedDateTime;
21  import java.time.format.DateTimeFormatter;
22  import java.util.Comparator;
23  import java.util.LinkedHashMap;
24  import java.util.Locale;
25  import java.util.Map;
26  import java.util.stream.Collectors;
27  
28  import org.apache.commons.text.StringEscapeUtils;
29  import org.apache.logging.log4j.LogManager;
30  import org.apache.logging.log4j.Logger;
31  import org.codelibs.core.lang.StringUtil;
32  import org.codelibs.fess.app.web.base.login.FessCredential;
33  import org.codelibs.fess.mylasta.action.FessUserBean;
34  import org.codelibs.fess.util.ComponentUtil;
35  import org.dbflute.optional.OptionalThing;
36  import org.lastaflute.web.login.credential.LoginCredential;
37  import org.lastaflute.web.util.LaRequestUtil;
38  
39  import jakarta.annotation.PostConstruct;
40  
41  /**
42   * The helper for user activities.
43   * This class provides methods to log user actions such as login, logout, and access.
44   * It supports both LTSV and ECS log formats.
45   *
46   */
47  public class ActivityHelper {
48  
49      /**
50       * Default constructor.
51       */
52      public ActivityHelper() {
53          // Default constructor
54      }
55  
56      /**
57       * The logger.
58       */
59      protected Logger logger = null;
60  
61      /**
62       * The logger name.
63       */
64      protected String loggerName = "fess.log.audit";
65  
66      /**
67       * The permission separator.
68       */
69      protected String permissionSeparator = "|";
70  
71      /**
72       * The flag to use ECS format.
73       */
74      protected boolean useEcsFormat = false;
75  
76      /**
77       * The ECS version.
78       */
79      protected String ecsVersion = "1.2.0";
80  
81      /**
82       * The ECS service name.
83       */
84      protected String ecsServiceName = "fess";
85  
86      /**
87       * The ECS event dataset.
88       */
89      protected String ecsEventDataset = "app";
90  
91      /**
92       * The environment map.
93       */
94      protected Map<String, String> envMap;
95  
96      /**
97       * Initialize the helper.
98       */
99      @PostConstruct
100     public void init() {
101         logger = LogManager.getLogger(loggerName);
102         final String logFormat = ComponentUtil.getFessConfig().getAppAuditLogFormat();
103         if (StringUtil.isBlank(logFormat)) {
104             useEcsFormat = "docker".equals(getEnvMap().get("FESS_APP_TYPE"));
105         } else if ("ecs".equals(logFormat)) {
106             useEcsFormat = true;
107         }
108     }
109 
110     /**
111      * Get the environment map.
112      * @return The environment map.
113      */
114     protected Map<String, String> getEnvMap() {
115         if (envMap != null) {
116             return envMap;
117         }
118         return System.getenv();
119     }
120 
121     /**
122      * Set the environment map.
123      * @param envMap The environment map.
124      */
125     public void setEnvMap(final Map<String, String> envMap) {
126         this.envMap = envMap;
127     }
128 
129     /**
130      * Log the login activity.
131      * @param user The user.
132      */
133     public void login(final OptionalThing<FessUserBean> user) {
134         final Map<String, String> valueMap = new LinkedHashMap<>();
135         valueMap.put("action", Action.LOGIN.name());
136         valueMap.put("user", user.map(FessUserBean::getUserId).orElse("-"));
137         valueMap.put("permissions",
138                 user.map(u -> stream(u.getPermissions()).get(stream -> stream.collect(Collectors.joining(permissionSeparator))))
139                         .filter(StringUtil::isNotBlank)
140                         .orElse("-"));
141         log(valueMap);
142     }
143 
144     /**
145      * Log the login failure activity.
146      * @param credential The credential.
147      */
148     public void loginFailure(final OptionalThing<LoginCredential> credential) {
149         final Map<String, String> valueMap = new LinkedHashMap<>();
150         valueMap.put("action", Action.LOGIN_FAILURE.name());
151         credential.ifPresent(c -> {
152             valueMap.put("class", c.getClass().getSimpleName());
153             if (c instanceof final FessCredential fessCredential) {
154                 valueMap.put("user", fessCredential.getUserId());
155             }
156         });
157         log(valueMap);
158     }
159 
160     /**
161      * Log the logout activity.
162      * @param user The user.
163      */
164     public void logout(final OptionalThing<FessUserBean> user) {
165         final Map<String, String> valueMap = new LinkedHashMap<>();
166         valueMap.put("action", Action.LOGOUT.name());
167         valueMap.put("user", user.map(FessUserBean::getUserId).orElse("-"));
168         valueMap.put("permissions",
169                 user.map(u -> stream(u.getPermissions()).get(stream -> stream.collect(Collectors.joining(permissionSeparator))))
170                         .filter(StringUtil::isNotBlank)
171                         .orElse("-"));
172         log(valueMap);
173     }
174 
175     /**
176      * Log the access activity.
177      * @param user The user.
178      * @param path The path.
179      * @param execute The execute.
180      */
181     public void access(final OptionalThing<FessUserBean> user, final String path, final String execute) {
182         final Map<String, String> valueMap = new LinkedHashMap<>();
183         valueMap.put("action", Action.ACCESS.name());
184         valueMap.put("user", user.map(FessUserBean::getUserId).orElse("-"));
185         valueMap.put("path", path);
186         valueMap.put("execute", execute);
187         log(valueMap);
188     }
189 
190     /**
191      * Log the permission changed activity.
192      * @param user The user.
193      */
194     public void permissionChanged(final OptionalThing<FessUserBean> user) {
195         final Map<String, String> valueMap = new LinkedHashMap<>();
196         valueMap.put("action", Action.UPDATE_PERMISSION.name());
197         valueMap.put("user", user.map(FessUserBean::getUserId).orElse("-"));
198         valueMap.put("permissions",
199                 user.map(u -> stream(u.getPermissions()).get(stream -> stream.collect(Collectors.joining(permissionSeparator))))
200                         .filter(StringUtil::isNotBlank)
201                         .orElse("-"));
202         log(valueMap);
203     }
204 
205     /**
206      * Log the script execution activity.
207      * @param scriptType The type of script (e.g., "groovy").
208      * @param script The script content.
209      * @param source The source of execution (e.g., "scheduler:JobName").
210      * @param user The user who triggered the execution.
211      * @param result The execution result (e.g., "success" or "failure:ExceptionType").
212      */
213     public void scriptExecution(final String scriptType, final String script, final String source, final String user, final String result) {
214         if (!ComponentUtil.getFessConfig().isScriptAuditLogEnabled()) {
215             return;
216         }
217         final Map<String, String> valueMap = new LinkedHashMap<>();
218         valueMap.put("action", Action.SCRIPT_EXECUTION.name());
219         valueMap.put("scriptType", scriptType != null ? scriptType : "-");
220         valueMap.put("source", source != null ? source : "-");
221         valueMap.put("user", user != null ? user : "-");
222         valueMap.put("result", result != null ? result : "-");
223         valueMap.put("script", normalizeScript(script));
224         log(valueMap);
225     }
226 
227     /**
228      * Normalize script content for logging.
229      * Replaces control characters and truncates if too long.
230      * @param script The script content.
231      * @return The normalized script content.
232      */
233     protected String normalizeScript(final String script) {
234         if (script == null) {
235             return "-";
236         }
237         final int maxLength = ComponentUtil.getFessConfig().getScriptAuditLogMaxLengthAsInteger();
238         String normalized = script;
239         if (normalized.length() > maxLength) {
240             normalized = normalized.substring(0, maxLength - 3) + "...";
241         }
242         return normalized.replace('\n', ' ').replace('\r', ' ').replace('\t', '_');
243     }
244 
245     /**
246      * Log the access denied activity.
247      * @param user The user.
248      * @param path The path.
249      */
250     public void accessDenied(final OptionalThing<FessUserBean> user, final String path) {
251         final Map<String, String> valueMap = new LinkedHashMap<>();
252         valueMap.put("action", Action.ACCESS_DENIED.name());
253         valueMap.put("user", user.map(FessUserBean::getUserId).orElse("-"));
254         valueMap.put("path", path);
255         log(valueMap);
256     }
257 
258     /**
259      * Print the log.
260      * @param action The action.
261      * @param user The user.
262      * @param params The parameters.
263      */
264     public void print(final String action, final OptionalThing<FessUserBean> user, final Map<String, String> params) {
265         final Map<String, String> valueMap = new LinkedHashMap<>();
266         valueMap.put("action", action.replace('\t', '_').toUpperCase(Locale.ENGLISH));
267         valueMap.put("user", user.map(FessUserBean::getUserId).orElse("-"));
268         final Comparator<Map.Entry<String, String>> c = Comparator.comparing(Map.Entry::getKey);
269         params.entrySet().stream().sorted(c).forEach(e -> {
270             valueMap.put(e.getKey(), e.getValue().replace('\t', '_'));
271         });
272         log(valueMap);
273     }
274 
275     /**
276      * Log the value map.
277      * @param valueMap The value map.
278      */
279     protected void log(final Map<String, String> valueMap) {
280         valueMap.put("ip", getClientIp());
281         valueMap.put("time", DateTimeFormatter.ISO_INSTANT.format(ZonedDateTime.now()));
282         if (useEcsFormat) {
283             printByEcs(valueMap);
284         } else {
285             printByLtsv(valueMap);
286         }
287     }
288 
289     /**
290      * Print the log by LTSV.
291      * @param valueMap The value map.
292      */
293     protected void printByLtsv(final Map<String, String> valueMap) {
294         printLog(valueMap.entrySet().stream().map(e -> e.getKey() + ":" + e.getValue()).collect(Collectors.joining("\t")));
295     }
296 
297     /**
298      * Print the log by ECS.
299      * @param valueMap The value map.
300      */
301     protected void printByEcs(final Map<String, String> valueMap) {
302         final StringBuilder buf = new StringBuilder(100);
303         buf.append("{\"@timestamp\":\"").append(valueMap.remove("time")).append('"');
304         buf.append(",\"log.level\":\"INFO\"");
305         buf.append(",\"ecs.version\":\"").append(ecsVersion).append('"');
306         buf.append(",\"service.name\":\"").append(ecsServiceName).append('"');
307         buf.append(",\"event.dataset\":\"").append(ecsEventDataset).append('"');
308         buf.append(",\"process.thread.name\":\"").append(StringEscapeUtils.escapeJson(Thread.currentThread().getName())).append('"');
309         buf.append(",\"log.logger\":\"").append(StringEscapeUtils.escapeJson(this.getClass().getName())).append('"');
310         valueMap.entrySet()
311                 .stream()
312                 .forEach(e -> buf.append(",\"labels.")
313                         .append(e.getKey())
314                         .append("\":\"")
315                         .append(StringEscapeUtils.escapeJson(e.getValue()))
316                         .append('"'));
317         buf.append('}');
318         printLog(buf.toString());
319     }
320 
321     /**
322      * Print the log.
323      * @param message The message.
324      */
325     protected void printLog(final String message) {
326         logger.info(message);
327     }
328 
329     /**
330      * Get the client IP.
331      * @return The client IP.
332      */
333     protected String getClientIp() {
334         return LaRequestUtil.getOptionalRequest().map(req -> ComponentUtil.getViewHelper().getClientIp(req)).orElse("-");
335     }
336 
337     /**
338      * The action.
339      */
340     protected enum Action {
341         /**
342          * The login action.
343          */
344         LOGIN,
345         /**
346          * The logout action.
347          */
348         LOGOUT,
349         /**
350          * The access action.
351          */
352         ACCESS,
353         /**
354          * The login failure action.
355          */
356         LOGIN_FAILURE,
357         /**
358          * The update permission action.
359          */
360         UPDATE_PERMISSION,
361         /**
362          * The script execution action.
363          */
364         SCRIPT_EXECUTION,
365         /**
366          * The access denied action.
367          */
368         ACCESS_DENIED;
369     }
370 
371     /**
372      * Set the logger name.
373      * @param loggerName The logger name.
374      */
375     public void setLoggerName(final String loggerName) {
376         this.loggerName = loggerName;
377     }
378 
379     /**
380      * Set the permission separator.
381      * @param permissionSeparator The permission separator.
382      */
383     public void setPermissionSeparator(final String permissionSeparator) {
384         this.permissionSeparator = permissionSeparator;
385     }
386 
387     /**
388      * Set the ECS version.
389      * @param ecsVersion The ECS version.
390      */
391     public void setEcsVersion(final String ecsVersion) {
392         this.ecsVersion = ecsVersion;
393     }
394 
395     /**
396      * Set the ECS service name.
397      * @param ecsServiceName The ECS service name.
398      */
399     public void setEcsServiceName(final String ecsServiceName) {
400         this.ecsServiceName = ecsServiceName;
401     }
402 
403     /**
404      * Set the ECS event dataset.
405      * @param ecsEventDataset The ECS event dataset.
406      */
407     public void setEcsEventDataset(final String ecsEventDataset) {
408         this.ecsEventDataset = ecsEventDataset;
409     }
410 }