View Javadoc
1   /*
2    * Copyright (c) 2002-2025, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.portal.service.security.impl;
35  
36  import java.nio.charset.Charset;
37  import java.nio.charset.StandardCharsets;
38  import java.security.MessageDigest;
39  import java.security.NoSuchAlgorithmException;
40  
41  import com.fasterxml.jackson.core.JsonProcessingException;
42  import com.fasterxml.jackson.databind.ObjectMapper;
43  import fr.paris.lutece.api.user.User;
44  import fr.paris.lutece.portal.service.security.AccessLoggerConstants;
45  import fr.paris.lutece.portal.service.util.AppPropertiesService;
46  import fr.paris.lutece.portal.service.security.IAccessLogger;
47  
48  import java.text.MessageFormat;
49  import org.apache.logging.log4j.Level;
50  import org.apache.logging.log4j.LogManager;
51  import org.apache.logging.log4j.Logger;
52  
53  /**
54   * This class provides a default implementation for AccessLogger Service
55   * 
56   * A specific log format can be set in the properties file
57   * 
58   * - portal.defaultAccessLogger.messageFormat - portal.defaultAccessLogger.messageFormatSeparator
59   * 
60   * Log lines could be certified with a hash, that could be verified, whith this property set to true :
61   * 
62   * - portal.defaultAccessLogger.addHashToLogs
63   * 
64   */
65  public final class DefaultAccessLogger implements IAccessLogger
66  {
67  
68      private static final Charset CONSTANT_HASH_ENCODING = StandardCharsets.UTF_8;
69      private static final String CONSTANT_HASH_DIGEST = "SHA-256";
70      // Amount of characters in the hashed value
71      private static final int CONSTANT_HASH_CHARACTERS_COUNT = 64;
72  
73      private static final String PROPERTY_ADD_HASH_TO_LOGS = "accessLogger.defaultAccessLogger.addHashToLogs";
74      private static final String PROPERTY_ACCESSLOG_MESSAGE_FORMAT = "accessLogger.defaultAccessLogger.messageFormat";
75      private static final String PROPERTY_ACCESSLOG_MESSAGE_FORMAT_SEPARATOR = "accessLogger.defaultAccessLogger.messageFormatSeparator";
76  
77      private static final String DEFAULT_ACCESSLOG_MESSAGE_FORMAT = "|{0}|{1}|{2}|{3}|{4}|{5}|";
78      private static final String DEFAULT_ACCESSLOG_MESSAGE_FORMAT_SEPARATOR = "|";
79  
80      private static final String ERROR_MSG = "ERROR : unable to create json from data";
81  
82      private final boolean _bAddHashToLogs = AppPropertiesService.getPropertyBoolean( PROPERTY_ADD_HASH_TO_LOGS, false );
83      private final String _messageFormat = AppPropertiesService.getProperty( PROPERTY_ACCESSLOG_MESSAGE_FORMAT, DEFAULT_ACCESSLOG_MESSAGE_FORMAT );
84      private final String _messageFormatSeparator = AppPropertiesService.getProperty( PROPERTY_ACCESSLOG_MESSAGE_FORMAT_SEPARATOR,
85              DEFAULT_ACCESSLOG_MESSAGE_FORMAT_SEPARATOR );
86  
87      public static final String DEFAULT_LOGGER_ACCESS_LOG = "lutece.accessLogger";
88      private static Logger _defaultLogger = LogManager.getLogger( DEFAULT_LOGGER_ACCESS_LOG );
89  
90      /**
91       * {@inheritDoc}
92       */
93      @Override
94      public void info( String strEventType, String strAppEventCode, User connectedUser, Object data, String specificOrigin )
95      {
96          Logger logger = getLogger( specificOrigin );
97  
98          if ( logger.isInfoEnabled( ) )
99          {
100             String strAppId = AppPropertiesService.getProperty( AccessLoggerConstants.PROPERTY_SITE_CODE, "?" );
101             String logMessage = getLogMessage( strAppId, strEventType, strAppEventCode, ( connectedUser != null ? connectedUser.getAccessCode( ) : "null" ),
102                     data );
103 
104             logger.info( logMessage );
105         }
106     }
107 
108     /**
109      * {@inheritDoc}
110      */
111     @Override
112     public void debug( String strEventType, String strAppEventCode, User connectedUser, Object data, String specificOrigin )
113     {
114         Logger logger = getLogger( specificOrigin );
115 
116         if ( logger.isDebugEnabled( ) )
117         {
118             String strAppId = AppPropertiesService.getProperty( AccessLoggerConstants.PROPERTY_SITE_CODE, "?" );
119             String logMessage = getLogMessage( strAppId, strEventType, strAppEventCode, ( connectedUser != null ? connectedUser.getAccessCode( ) : "null" ),
120                     data );
121 
122             logger.debug( logMessage );
123         }
124     }
125 
126     /**
127      * {@inheritDoc}
128      */
129     @Override
130     public void trace( String strEventType, String strAppEventCode, User connectedUser, Object data, String specificOrigin )
131     {
132         Logger logger = getLogger( specificOrigin );
133 
134         if ( logger.isTraceEnabled( ) )
135         {
136             String strAppId = AppPropertiesService.getProperty( AccessLoggerConstants.PROPERTY_SITE_CODE, "?" );
137             String logMessage = getLogMessage( strAppId, strEventType, strAppEventCode, ( connectedUser != null ? connectedUser.getAccessCode( ) : "null" ),
138                     data );
139 
140             logger.trace( logMessage );
141         }
142     }
143 
144     /**
145      * {@inheritDoc}
146      */
147     @Override
148     public void warn( String strEventType, String strAppEventCode, User connectedUser, Object data, String specificOrigin )
149     {
150         Logger logger = getLogger( specificOrigin );
151 
152         if ( logger.isEnabled( Level.WARN ) )
153         {
154             String strAppId = AppPropertiesService.getProperty( AccessLoggerConstants.PROPERTY_SITE_CODE, "?" );
155             String logMessage = getLogMessage( strAppId, strEventType, strAppEventCode, ( connectedUser != null ? connectedUser.getAccessCode( ) : "null" ),
156                     data );
157 
158             logger.warn( logMessage );
159         }
160     }
161 
162     /**
163      * build log message
164      *
165      * @param eventType
166      * @param description
167      * @param connectedUserLogin
168      * @param data
169      * @return the log message
170      */
171     private String getLogMessage( String strAppId, String strEventType, String strAppEventCode, String strConnectedUserLogin, Object data )
172     {
173 
174         String jsonData = "";
175 
176         if ( data != null )
177         {
178             ObjectMapper obj = new ObjectMapper( );
179 
180             try
181             {
182                 jsonData = obj.writeValueAsString( data );
183             }
184             catch( JsonProcessingException e )
185             {
186                 jsonData = ERROR_MSG;
187             }
188         }
189 
190         return getLogMessage( strAppId, strEventType, strAppEventCode, strConnectedUserLogin, jsonData, isAddHashToLogs( ) );
191     }
192 
193     /**
194      * build log message
195      *
196      * @param eventType
197      * @param description
198      * @param connectedUserLogin
199      * @param data
200      * @return the log message
201      */
202     private String getLogMessage( String strAppId, String strEventType, String strAppEventCode, String strConnectedUserLogin, String strData,
203             boolean isAddHashToLogs )
204     {
205 
206         String strHash = "";
207 
208         if ( isAddHashToLogs )
209         {
210             strHash = getHash( MessageFormat.format( _messageFormat, "", strAppId, strEventType, strAppEventCode, strConnectedUserLogin, strData ) );
211         }
212 
213         return MessageFormat.format( _messageFormat, strHash, strAppId, strEventType, strAppEventCode, strConnectedUserLogin, strData );
214     }
215 
216     /**
217      * get hash
218      *
219      * @param message
220      * @param last
221      *            hash
222      *
223      * @return the hash in String
224      */
225     private static String getHash( String message )
226     {
227 
228         byte [ ] byteChaine;
229         try
230         {
231             byteChaine = message.getBytes( CONSTANT_HASH_ENCODING );
232             MessageDigest md = MessageDigest.getInstance( CONSTANT_HASH_DIGEST );
233             byte [ ] hash = md.digest( byteChaine );
234 
235             // convert byte array to Hexadecimal String
236             StringBuilder sb = new StringBuilder( 2 * hash.length );
237             for ( byte b : hash )
238             {
239                 sb.append( String.format( "%02x", b & 0xff ) );
240             }
241 
242             return sb.toString( );
243 
244         }
245         catch( NoSuchAlgorithmException e )
246         {
247             return "Hash ERROR : " + e.getLocalizedMessage( );
248         }
249 
250     }
251 
252     /**
253      * verify hash
254      *
255      * @param message
256      *
257      * @return true if the hash contained in the message is valid
258      */
259     public boolean verifyMessageHash( String message )
260     {
261         try
262         {
263 
264             int idx = message.indexOf( _messageFormatSeparator, message.indexOf( DEFAULT_LOGGER_ACCESS_LOG ) );
265             // Get the hash's value
266             String hash = message.substring( idx + 1, idx + CONSTANT_HASH_CHARACTERS_COUNT + 1 );
267             // Get the actual data to verify
268             String data = "||" + message.substring( idx + CONSTANT_HASH_CHARACTERS_COUNT + 2 );
269 
270             return ( hash != null && ( hash.equals( "" ) || hash.equals( getHash( data ) ) ) );
271 
272         }
273         catch( StringIndexOutOfBoundsException e )
274         {
275 
276             return false;
277         }
278     }
279 
280     /**
281      * is addHashToLogs enabled
282      * 
283      * @return true if addHashToLogs enabled
284      */
285     public boolean isAddHashToLogs( )
286     {
287         return _bAddHashToLogs;
288     }
289 
290     /**
291      * get logger
292      * 
293      * @param strSpecificLogger
294      * @return the logger
295      */
296     private Logger getLogger( String specificOrigin )
297     {
298         if ( specificOrigin != null && !"".equals( specificOrigin ) )
299         {
300             return LogManager.getLogger( DEFAULT_LOGGER_ACCESS_LOG + "." + specificOrigin );
301         }
302 
303         return _defaultLogger;
304 
305     }
306 }