From 552ef6e55189debb653ad836eab712c453bf3c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81mi=20Zara?= Date: Tue, 11 Nov 2025 16:09:30 +0100 Subject: [PATCH] LUT-30963: Make SecurityHeaderService threadsafe Use a volatile cache object, which is swapped for a new one when the cache needs to be refreshed. Implement StartUpService to populate the cache on startup. Replace the cached map of security header by a collection. The map was only used for debug logs in low traffic methods. Simplify the construction of the map of security headers by type and category. --- .../securityheader/SecurityHeaderService.java | 359 +++++++++--------- 1 file changed, 179 insertions(+), 180 deletions(-) diff --git a/src/java/fr/paris/lutece/portal/service/securityheader/SecurityHeaderService.java b/src/java/fr/paris/lutece/portal/service/securityheader/SecurityHeaderService.java index 6e2661135e..b7a944a533 100644 --- a/src/java/fr/paris/lutece/portal/service/securityheader/SecurityHeaderService.java +++ b/src/java/fr/paris/lutece/portal/service/securityheader/SecurityHeaderService.java @@ -50,40 +50,83 @@ import fr.paris.lutece.portal.business.securityheader.SecurityHeaderHome; import fr.paris.lutece.portal.business.securityheader.SecurityHeaderPageCategory; import fr.paris.lutece.portal.business.securityheader.SecurityHeaderType; +import fr.paris.lutece.portal.service.init.StartUpService; import fr.paris.lutece.util.ReferenceList; /** * This class provides a service that offers methods to manage security headers. */ -public class SecurityHeaderService +public class SecurityHeaderService implements StartUpService { + + /** + * Cache of security headers + */ + private static final class Cache + { + private final Collection _securityHeaders; + + private final Map>> _mapActiveSecurityHeadersForFilters; + + private Cache( ) + { + _securityHeaders = SecurityHeaderHome.findAll( ); + + _mapActiveSecurityHeadersForFilters = new HashMap>>( ); + _securityHeaders.stream( ).filter( SecurityHeader::isActive ).forEach( securityHeader -> { + _mapActiveSecurityHeadersForFilters + .computeIfAbsent( securityHeader.getType( ), t -> new HashMap<>( ) ) + .computeIfAbsent( SecurityHeaderType.PAGE.getCode( ).equals( securityHeader.getType( ) ) + ? securityHeader.getPageCategory( ) + : null, c -> new ArrayList<>( ) ) + .add( securityHeader ); + } ); + } + + /** + * All cached security headers + * + * @return all security headers + */ + private Collection getSecurityHeaders( ) + { + return _securityHeaders; + } + + /** + * Cached active security headers, by type and page category + * + * @return active security headers, by type and page category + */ + private Map>> getMapActiveSecurityHeadersForFilters( ) + { + return _mapActiveSecurityHeadersForFilters; + } + } + + /** + * The current cache. Marked volatile so that if can be swapped + * atomically with a fresher cache. + * + * This field should be read only once for a coherent interaction group of actions. + */ + private volatile Cache _cache; - //This map contains the security headers loaded from the database. It prevents to make database calls when data are up to date. - //It must be refreshed each time that an action is performed. Currently, These actions are creating a new header, modifying an - //existing header, removing a header and enabling or disabling a header - private Map _mapSecurityHeaders = new HashMap( ); - - Map>> _mapActiveSecurityHeadersForFilters = new HashMap>>( ); - - //This boolean indicates if a refresh has been done on the security headers map. In other words, it tells if the map is up to date - //with data from database - private boolean _bMapRefreshDone = false; - - private Logger _logger = LogManager.getLogger( "lutece.securityHeader" ); + private final Logger _logger = LogManager.getLogger( "lutece.securityHeader" ); /** - * Returns all the security headers that match specified name, type and page category (if not null for the latter). - * If page category is null (REST api case), this criterion is ignored. + * Returns all the security headers that match specified name, type and page category. + * If page category is null (REST API case), this criterion is ignored. * * @param strName * The name of the security header * @param strType * The type of the security header - * @param pageCategory + * @param strPageCategory * The page category of the security header - * @return list od security headers matching the criteria + * @return list of security headers matching the criteria */ - public List find( String strName, String strType, String pageCategory ) + public List find( String strName, String strType, String strPageCategory ) { ArrayList securityHeadersResultList = new ArrayList( ); @@ -91,7 +134,7 @@ public List find( String strName, String strType, String pageCat { if( securityHeader.getName( ).equals( strName ) && securityHeader.getType( ).equals( strType ) ) { - if( pageCategory == null || securityHeader.getPageCategory( ).equals( pageCategory ) ) + if( strPageCategory == null || securityHeader.getPageCategory( ).equals( strPageCategory ) ) { securityHeadersResultList.add( securityHeader ); } @@ -101,48 +144,35 @@ public List find( String strName, String strType, String pageCat return securityHeadersResultList; } - /** - * Returns all security headers from the specified type and the specified category (if not null) - * that are active (attribute is_enable = true). - * - * @param strType - * @param strPageCategory - * @return collection on security headers - */ - public Collection findActive( String strType, String strPageCategory ) - { - if( !_bMapRefreshDone ) - { - refreshSecurityHeadersMap( SecurityHeaderHome.findAll() ); - } - if( _mapActiveSecurityHeadersForFilters.get(strType) != null ) - { - return _mapActiveSecurityHeadersForFilters.get( strType ).get( strPageCategory ); - } - return null; - } + /** + * Returns all security headers of the specified type and the specified + * category (if not null) that are active (attribute is_enable + * = true). + * + * @param strType + * the type of security header to find + * @param strPageCategory + * the page category of security header to find + * @return collection on security headers + */ + public Collection findActive( String strType, String strPageCategory ) + { + Map>> map = _cache.getMapActiveSecurityHeadersForFilters( ); + if ( map.get( strType ) == null ) + { + return null; + } + return Collections.unmodifiableCollection( map.get( strType ).get( strPageCategory ) ); + } /** * Returns a collection of all security headers. - * Security headers are fetched from database (if data are stale) or from security headers map in memory (if the data in map are still up to date). * * @return collection of security headers */ public Collection findAll( ) { - Collection securityHeadersList = null; - - if( _bMapRefreshDone ) - { - securityHeadersList = _mapSecurityHeaders.values( ); - } - else - { - securityHeadersList = SecurityHeaderHome.findAll( ); - refreshSecurityHeadersMap( securityHeadersList ); - } - - return securityHeadersList; + return Collections.unmodifiableCollection( _cache.getSecurityHeaders( ) ); } /** @@ -195,7 +225,7 @@ public ReferenceList getPageCategoryList( ) } /** - * Create the security header given in parameter. Like all the actions, this one invalidates the map containing the security headers. + * Create the security header given in parameter. * * @param securityHeader * The security header to create @@ -203,13 +233,13 @@ public ReferenceList getPageCategoryList( ) public void create( SecurityHeader securityHeader ) { _logger.debug( "Security header to create : name : {}, value : {}, type : {}, page category : {}", securityHeader.getName( ), securityHeader.getValue( ), securityHeader.getType( ), securityHeader.getPageCategory( ) ); - SecurityHeaderHome.create( securityHeader ); - clearMapSecurityHeaders( ); - _logger.debug( "Security header created" ); + SecurityHeaderHome.create( securityHeader ); + _logger.debug( "Security header created" ); + refreshSecurityHeadersCache( ); } /** - * Update the security header given in parameter. Like all the actions, this one invalidates the map containing the security headers. + * Update the security header given in parameter. * * @param securityHeader * The security header to update @@ -217,127 +247,96 @@ public void create( SecurityHeader securityHeader ) public void update( SecurityHeader securityHeader ) { _logger.debug( "Security header to update : id : {}, name : {}, value : {}, type : {}, page category : {}", securityHeader.getId( ), securityHeader.getName( ), securityHeader.getValue( ), securityHeader.getType( ), securityHeader.getPageCategory( ) ); - SecurityHeaderHome.update( securityHeader ); - clearMapSecurityHeaders( ); - _logger.debug( "Security header updated" ); - } - - /** - * Remove the security header given in parameter. Like all the actions, this one invalidates the map containing the security headers. - * - * @param securityHeader - * The security header to remove - */ - public void remove( int nSecurityHeaderId ) - { - SecurityHeader securityHeader = _mapSecurityHeaders.get( String.valueOf( nSecurityHeaderId ) ); - _logger.debug( "Security header to delete : id : {}, name : {}, value : {}, type : {}, page category : {}", securityHeader.getId( ), securityHeader.getName( ), securityHeader.getValue( ), securityHeader.getType( ), securityHeader.getPageCategory( ) ); - SecurityHeaderHome.remove( nSecurityHeaderId ); - clearMapSecurityHeaders( ); - _logger.debug( "Security header deleted" ); - } - - /** - * Enable the security header with the id given in parameter. Like all the actions, this one invalidates the map containing the security headers. - * - * @param nSecurityHeaderId - * The id of the security header to enable - */ - public void enable( int nSecurityHeaderId ) - { - SecurityHeader securityHeader = _mapSecurityHeaders.get( String.valueOf( nSecurityHeaderId ) ); - _logger.debug( "Security header to enable : id : {}, name : {}, value : {}, type : {}, page category : {}", securityHeader.getId( ), securityHeader.getName( ), securityHeader.getValue( ), securityHeader.getType( ), securityHeader.getPageCategory( ) ); - SecurityHeaderHome.updateIsActive( nSecurityHeaderId, true ); - clearMapSecurityHeaders( ); - _logger.debug( "Security header enabled" ); - } - - /** - * Disable the security header with the id given in parameter. Like all the actions, this one invalidates the map containing the security headers. - * - * @param nSecurityHeaderId - * The id of the security header to disable - */ - public void disable( int nSecurityHeaderId ) - { - SecurityHeader securityHeader = _mapSecurityHeaders.get( String.valueOf( nSecurityHeaderId ) ); - _logger.debug( "Security header to disable : id : {}, name : {}, value : {}, type : {}, page category : {}", securityHeader.getId( ), securityHeader.getName( ), securityHeader.getValue( ), securityHeader.getType( ), securityHeader.getPageCategory( ) ); - SecurityHeaderHome.updateIsActive( nSecurityHeaderId, false ); - clearMapSecurityHeaders( ); - _logger.debug( "Security header disabled" ); - } - - /** - * Clears the map containing the security headers - * - */ - private void clearMapSecurityHeaders( ) - { - _mapSecurityHeaders.clear( ); - _mapActiveSecurityHeadersForFilters.clear( ); - _bMapRefreshDone = false; - _logger.debug( "Security header maps cleared" ); - } - - /** - * Refreshes the map of security headers with the list given in parameter. After a call to this method, data of the map are up to date with database data. - * - * @param securityHeadersList - * The security headers collection - */ - private void refreshSecurityHeadersMap( Collection securityHeadersList ) - { - for( SecurityHeader securityHeader : securityHeadersList ) - { - _mapSecurityHeaders.put( String.valueOf( securityHeader.getId( ) ), securityHeader ); - - if( securityHeader.isActive() ) - { - _mapActiveSecurityHeadersForFilters.put( securityHeader.getType( ), addHeaderToTypeMap( securityHeader ) ); - } - } - _bMapRefreshDone = true; - _logger.debug( "Security header map refreshed" ); - } - - /** - * Adds a security header in the map of active security headers of the same type as the security header passed in parameter - * - * @param securityHeader Security header to add to the map - * @return map of active security headers of the same type as the security header passed in parameter updated with this security header - */ - private Map> addHeaderToTypeMap( SecurityHeader securityHeader ) - { - //In _mapActiveSecurityHeadersForFilters, 2 keys are necessary to retrieve a list of security headers. - //For Page headers, those keys are respectively type and page category - //For Rest api headers, those keys are respectively type and null value. - //As page category is not irrelevant for rest api headers, they are grouped using the null key. - String firstKey = securityHeader.getType( ); - Map> mapHeadersForType = _mapActiveSecurityHeadersForFilters.get( firstKey ); - List headersListToUpdate = null; - - String secondKey = null; - if( securityHeader.getType( ).equals( SecurityHeaderType.PAGE.getCode( ) ) ) - { - secondKey = securityHeader.getPageCategory( ); - } - - if( mapHeadersForType == null ) - { - mapHeadersForType = new HashMap>( ); - headersListToUpdate = new ArrayList( ); - } - else - { - headersListToUpdate = mapHeadersForType.get( secondKey ); - if( headersListToUpdate == null ) - { - headersListToUpdate = new ArrayList( ); - } - } - headersListToUpdate.add( securityHeader ); - mapHeadersForType.put( secondKey, headersListToUpdate ); - - return mapHeadersForType; + SecurityHeaderHome.update( securityHeader ); + _logger.debug( "Security header updated" ); + refreshSecurityHeadersCache( ); } + + /** + * Remove the security header given in parameter. + * + * @param securityHeader + * The security header to remove + */ + public void remove( int nSecurityHeaderId ) + { + logOperation( nSecurityHeaderId, "remove" ); + SecurityHeaderHome.remove( nSecurityHeaderId ); + _logger.debug( "Security header deleted" ); + refreshSecurityHeadersCache( ); + } + + /** + * Enable the security header with the id given in parameter. + * + * @param nSecurityHeaderId + * The id of the security header to enable + */ + public void enable( int nSecurityHeaderId ) + { + logOperation( nSecurityHeaderId, "enable" ); + SecurityHeaderHome.updateIsActive( nSecurityHeaderId, true ); + _logger.debug( "Security header enabled" ); + refreshSecurityHeadersCache( ); + } + + /** + * Disable the security header with the id given in parameter. + * + * @param nSecurityHeaderId + * The id of the security header to disable + */ + public void disable( int nSecurityHeaderId ) + { + logOperation( nSecurityHeaderId, "disable" ); + SecurityHeaderHome.updateIsActive( nSecurityHeaderId, false ); + _logger.debug( "Security header disabled" ); + refreshSecurityHeadersCache( ); + } + + private void logOperation( int nSecurityHeaderId, String strOperation ) + { + if ( _logger.isDebugEnabled( ) ) + { + SecurityHeader securityHeader = SecurityHeaderHome.findByPrimaryKey( nSecurityHeaderId ); + if ( securityHeader != null ) + { + _logger.debug( "Security header to {} : id : {}, name : {}, value : {}, type : {}, page category : {}", + strOperation, securityHeader.getId( ), securityHeader.getName( ), securityHeader.getValue( ), + securityHeader.getType( ), securityHeader.getPageCategory( ) ); + } + else + { + _logger.debug( "Non existent security header to disable : id : {}", nSecurityHeaderId ); + } + } + } + + /** + * Refreshes the cache of security headers. After a call to this method, data + * in the cache is up to date with database data. + */ + private void refreshSecurityHeadersCache( ) + { + _cache = new Cache( ); + _logger.debug( "Security headers cache refreshed" ); + } + + @Override + public String getName( ) + { + return "SecurityHeaderService"; + } + + /** + * {@inheritDoc} + * + *

This initialize the cache after the Spring context has been initialized, + * so that all necessary infrastructure is available + */ + @Override + public void process( ) + { + refreshSecurityHeadersCache( ); + } } \ No newline at end of file