SecurityHeaderJspBean.java
/*
* Copyright (c) 2002-2022, City of Paris
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice
* and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice
* and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* License 1.0
*/
package fr.paris.lutece.portal.web.system;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import fr.paris.lutece.portal.business.securityheader.SecurityHeader;
import fr.paris.lutece.portal.business.securityheader.SecurityHeaderHome;
import fr.paris.lutece.portal.business.securityheader.SecurityHeaderType;
import fr.paris.lutece.portal.service.admin.AccessDeniedException;
import fr.paris.lutece.portal.service.message.AdminMessage;
import fr.paris.lutece.portal.service.message.AdminMessageService;
import fr.paris.lutece.portal.service.security.SecurityTokenService;
import fr.paris.lutece.portal.service.securityheader.SecurityHeaderService;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.portal.service.template.AppTemplateService;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.util.mvc.admin.MVCAdminJspBean;
import fr.paris.lutece.util.ErrorMessage;
import fr.paris.lutece.util.ReferenceItem;
import fr.paris.lutece.util.ReferenceList;
import fr.paris.lutece.util.html.HtmlTemplate;
import fr.paris.lutece.util.http.SecurityUtil;
/**
* This class provides the user interface to manage security headers features ( manage, create, modify, remove, activate/deactivate ).
*/
public class SecurityHeaderJspBean extends MVCAdminJspBean
{
// Rights
public static final String RIGHT_SECURITY_HEADER_MANAGEMENT = "CORE_SECURITY_HEADER_MANAGEMENT";
// Templates
private static final String TEMPLATE_CREATE_SECURITYHEADER = "admin/system/create_securityheader.html";
private static final String TEMPLATE_MODIFY_SECURITYHEADER = "admin/system/modify_securityheader.html";
// Markers
private static final String MARK_SECURITY_HEADERS_LIST = "security_headers_list";
private static final String MARK_SECURITY_HEADER = "securityheader";
private static final String MARK_TYPES_LIST = "types_list";
private static final String MARK_TYPE_SELECTED = "selected_type";
private static final String MARK_PAGE_CATEGORY_LIST = "page_category_list";
private static final String MARK_PAGE_CATEGORY_SELECTED = "selected_pageCategory";
private static final String MARK_ERRORS = "errors";
// Properties
private static final String PROPERTY_CREATE_SECURITYHEADER_PAGETITLE = "portal.securityheader.create_securityheader.pageTitle";
private static final String PROPERTY_MODIFY_SECURITYHEADER_PAGETITLE = "portal.securityheader.modify_securityheader.pageTitle";
private static final String MESSAGE_ACTIVE_HEADER_NOT_EDITABLE = "portal.securityheader.message.activeHeaderNotEditable";
private static final String MESSAGE_CONFIRM_REMOVE = "portal.securityheader.message.confirmRemoveSecurityHeader";
private static final String MESSAGE_HEADER_ALREADY_EXIST = "portal.securityheader.message.securityHeadersAlreadyexists";
private static final String MESSAGE_PAGE_CATEGORY_REQUIRED_WHEN_PAGE_IS_TYPE = "portal.securityheader.message.pageCategoryRequiredTypePage";
private static final String MESSAGE_TYPE_UNKNOWN = "portal.securityheader.message.typeUnknown";
private static final String MESSAGE_PAGE_CATEGORY_UNKNOWN = "portal.securityheader.message.pageCategoryUnknown";
// Validations
private static final String VALIDATION_ATTRIBUTES_PREFIX = "portal.securityheader.model.entity.securityheader.attribute.";
// Template Files path
private static final String TEMPLATE_MANAGE_SECURITY_HEADERS = "admin/system/manage_security_headers.html";
// Parameters
private static final String PARAMETER_SECURITY_HEADER_ID = "id_securityheader";
private static final String PARAMETER_NAME = "name";
private static final String PARAMETER_VALUE = "value";
private static final String PARAMETER_DESCRIPTION = "description";
private static final String PARAMETER_TYPE = "type";
private static final String PARAMETER_PAGE_CATEGORY = "pageCategory";
private static final String PARAMETER_ACTION = "action";
// Jsp definition
public static final String JSP_MANAGE_SECURITY_HEADERS = "ManageSecurityHeaders.jsp";
public static final String JSP_REMOVE_SECURITY_HEADERS = "jsp/admin/system/DoRemoveSecurityHeader.jsp";
// Actions
private static final String ACTION_ENABLE = "ENABLE";
private static final String ACTION_DISABLE = "DISABLE";
private static final long serialVersionUID = 7010476999488231065L;
/**
* Returns the page to manage security headers.
*
* @param request
* The HttpServletRequest
* @return The HTML code.
*/
public String getManageSecurityHeaders( HttpServletRequest request )
{
HashMap<String, Object> model = createModelForHeadersList( request );
HtmlTemplate template = AppTemplateService.getTemplate( TEMPLATE_MANAGE_SECURITY_HEADERS, getLocale( ), model );
return getAdminPage( template.getHtml( ) );
}
/**
*
* Creates the model used for displaying security headers list in manage security headers page.
*
* @param request
* The HttpServletRequest
* @return model map
*/
private HashMap<String, Object> createModelForHeadersList( HttpServletRequest request )
{
HashMap<String, Object> model = new HashMap<>( );
model.put( MARK_SECURITY_HEADERS_LIST, getSecurityHeaderService( ).findAllSorted( getLocale( ) ) );
model.put( SecurityTokenService.MARK_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MANAGE_SECURITY_HEADERS ) );
return model;
}
/**
* Returns the security header creation page.
*
* @param request
* the http request
* @return the html code for the securityheader creation page
*/
public String getCreateSecurityHeader( HttpServletRequest request )
{
setPageTitleProperty( PROPERTY_CREATE_SECURITYHEADER_PAGETITLE );
HashMap<String, Object> model = createModelForHeaderCreation( request );
HtmlTemplate template = AppTemplateService.getTemplate( TEMPLATE_CREATE_SECURITYHEADER, getLocale( ), model );
return getAdminPage( template.getHtml( ) );
}
/**
*
* Creates the model used for adding a new security header.
*
* @param request
* The HttpServletRequest
* @return model map
*/
private HashMap<String, Object> createModelForHeaderCreation( HttpServletRequest request )
{
ReferenceList listTypes = getSecurityHeaderService( ).getTypeList( );
ReferenceList listPageCategories = getSecurityHeaderService( ).getPageCategoryList( );
HashMap<String, Object> model = new HashMap<>( );
model.put( MARK_TYPES_LIST, listTypes );
model.put( MARK_PAGE_CATEGORY_LIST, listPageCategories );
if ( !listTypes.isEmpty( ) )
{
model.put( MARK_TYPE_SELECTED, listTypes.get( 0 ).getCode( ) );
}
if ( !listPageCategories.isEmpty( ) )
{
model.put( MARK_PAGE_CATEGORY_SELECTED, listPageCategories.get( 0 ).getCode( ) );
}
model.put( SecurityTokenService.MARK_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_CREATE_SECURITYHEADER ) );
return model;
}
/**
* Process the data capture form for create a security header
*
* @param request
* The HTTP Request
* @return The Jsp URL of the process result
* @throws AccessDeniedException
* If the security token is invalid
*/
public String doCreateSecurityHeader( HttpServletRequest request ) throws AccessDeniedException
{
SecurityHeader securityHeader = new SecurityHeader( );
String strErrors = processCreationFormData( request, securityHeader );
if ( strErrors != null )
{
return AdminMessageService.getMessageUrl( request, strErrors, AdminMessage.TYPE_STOP );
}
if ( !SecurityTokenService.getInstance( ).validate( request, TEMPLATE_CREATE_SECURITYHEADER ) )
{
throw new AccessDeniedException( ERROR_INVALID_TOKEN );
}
getSecurityHeaderService( ).create( securityHeader );
return JSP_MANAGE_SECURITY_HEADERS;
}
/**
* Process Creation Form Data.
*
* @param request
* The HTTP request
* @param securityheader
* The security header
* @return An Error message or null if no error
*/
private String processCreationFormData( HttpServletRequest request, SecurityHeader securityHeader )
{
securityHeader.setName( request.getParameter( PARAMETER_NAME ) );
securityHeader.setValue( request.getParameter( PARAMETER_VALUE ) );
securityHeader.setDescription( request.getParameter( PARAMETER_DESCRIPTION ) );
securityHeader.setType( request.getParameter( PARAMETER_TYPE ) );
securityHeader.setPageCategory( getPageCategory( request.getParameter( PARAMETER_PAGE_CATEGORY ), securityHeader.getType( ) ) );
String strErrors = processCommonControls( securityHeader );
if( strErrors != null )
{
return strErrors;
}
if( !isSecurityHeaderToCreateUnique( securityHeader ) )
{
return MESSAGE_HEADER_ALREADY_EXIST;
}
return null;
}
/**
* Executes common controls between creation and modification actions on request parameters
*
* @param securityHeader
* Security header to control
* @return null if controls are passed, an string containing an error message otherwise
*/
private String processCommonControls( SecurityHeader securityHeader )
{
if( !validateBean( securityHeader, VALIDATION_ATTRIBUTES_PREFIX ) )
{
List<ErrorMessage> listErrors = ( List<ErrorMessage> ) getModel( ).get( MARK_ERRORS );
return listErrors.get( 0 ).getMessage( );
}
if( !isTypeBelongsToReferenceList ( securityHeader.getType( ) ) )
{
return MESSAGE_TYPE_UNKNOWN;
}
if( securityHeader.getPageCategory( ) != null && !isPageCategoryBelongsToReferenceList ( securityHeader.getPageCategory( ) ) )
{
return MESSAGE_PAGE_CATEGORY_UNKNOWN;
}
if( SecurityHeaderType.PAGE.getCode( ).equals( securityHeader.getType( ) ) && StringUtils.isEmpty( securityHeader.getPageCategory( ) ) )
{
return MESSAGE_PAGE_CATEGORY_REQUIRED_WHEN_PAGE_IS_TYPE;
}
return null;
}
/**
* Checks if specified type is found in the reference list of security headers types
*
* @param strType
* THe security header type
* @return true if type belongs to the reference types list, false otherwise
*/
private boolean isTypeBelongsToReferenceList( String strType )
{
for (ReferenceItem referenceType : getSecurityHeaderService( ).getTypeList( ) )
{
if ( strType.equals( referenceType.getCode() ) )
{
return true;
}
}
return false;
}
/**
* Checks if specified page category is found in the reference list of security headers page categories
*
* @param strPageCategory
* The security header page category
* @return true if page category belongs to the reference types list, false otherwise
*/
private boolean isPageCategoryBelongsToReferenceList( String strPageCategory )
{
for (ReferenceItem referencePageCategory : getSecurityHeaderService( ).getPageCategoryList( ) )
{
if ( strPageCategory.equals( referencePageCategory.getCode() ) )
{
return true;
}
}
return false;
}
/**
* Checks if the security header to create doesn't already exist to prevent duplicates.
* The criteria used to determine if a security header is unique are name, type and page category (in case of security header type is equals to page).
*
* @param securityHeaderToCreate
* The security header to create
* @return true if the security header doesn't exist, false otherwise
*/
private boolean isSecurityHeaderToCreateUnique( SecurityHeader securityHeaderToCreate )
{
return getSecurityHeaderService( ).find( securityHeaderToCreate.getName( ), securityHeaderToCreate.getType( ) , securityHeaderToCreate.getPageCategory( ) ).isEmpty( );
}
/**
* Returns the security header modification page.
*
* @param request
* the http request
* @return the html code for the securityheader modification page
*/
public String getModifySecurityHeader( HttpServletRequest request )
{
setPageTitleProperty( PROPERTY_MODIFY_SECURITYHEADER_PAGETITLE );
String strSecurityHeaderId = request.getParameter( PARAMETER_SECURITY_HEADER_ID );
if ( !StringUtils.isNumeric( strSecurityHeaderId ) )
{
AppLogService.error( " {} is not a valid security header id.", ( ) -> SecurityUtil.logForgingProtect( strSecurityHeaderId ) );
return getManageSecurityHeaders( request );
}
SecurityHeader securityHeader = getSecurityHeaderForModification( strSecurityHeaderId );
if ( securityHeader == null )
{
AppLogService.error( "{} is not a valid security header id.", ( ) -> SecurityUtil.logForgingProtect( strSecurityHeaderId ) );
return getManageSecurityHeaders( request );
}
HashMap<String, Object> model = createModelForHeaderModification( request, securityHeader );
HtmlTemplate template = AppTemplateService.getTemplate( TEMPLATE_MODIFY_SECURITYHEADER, getLocale( ), model );
return getAdminPage( template.getHtml( ) );
}
/**
* This method returns the security header to modify. Value field is escaped because some headers values can have double quotes
* (Clear-site-data header for instance) and in this case, the value is not displayed on the modification screen
*
* @param strSecurityHeaderId
* The security header id
* @return security header to modify
*/
private SecurityHeader getSecurityHeaderForModification( String strSecurityHeaderId )
{
SecurityHeader securityHeader = SecurityHeaderHome.findByPrimaryKey( Integer.parseInt( strSecurityHeaderId ) );
if( securityHeader != null )
{
securityHeader.setValue( StringEscapeUtils.escapeHtml4( securityHeader.getValue( ) ) );
}
return securityHeader;
}
/**
* Returns the message page that informs that a security header is not editable.
*
* @param request
* The Http Request
* @return the confirmation url
*/
public String getMessageNotEditableSecurityHeader( HttpServletRequest request )
{
return AdminMessageService.getMessageUrl( request, MESSAGE_ACTIVE_HEADER_NOT_EDITABLE, AdminMessage.TYPE_INFO );
}
/**
*
* Creates the model used for modifying a security header.
*
* @param request
* The HttpServletRequest
* @return model map
*/
private HashMap<String, Object> createModelForHeaderModification(HttpServletRequest request, SecurityHeader securityHeader)
{
ReferenceList listTypes = getSecurityHeaderService().getTypeList( );
ReferenceList listPageCategories = getSecurityHeaderService().getPageCategoryList( );
HashMap<String, Object> model = new HashMap<>( );
model.put( MARK_TYPES_LIST, listTypes );
model.put( MARK_PAGE_CATEGORY_LIST, listPageCategories );
model.put( MARK_TYPE_SELECTED, securityHeader.getType() );
String selectedCategory = null;
if(securityHeader.getType().equals(SecurityHeaderType.PAGE.getCode()))
{
selectedCategory = securityHeader.getPageCategory();
}
else
{
selectedCategory = listPageCategories.get( 0 ).getCode( );
}
model.put( MARK_PAGE_CATEGORY_SELECTED, selectedCategory );
model.put( MARK_SECURITY_HEADER, securityHeader );
model.put( SecurityTokenService.MARK_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MODIFY_SECURITYHEADER ) );
return model;
}
/**
* Processes the data capture form for modifying a security header.
*
* @param request
* The HTTP Request
* @return The Jsp URL of the process result
* @throws AccessDeniedException
* if the security token is invalid
*/
public String doModifySecurityHeader( HttpServletRequest request ) throws AccessDeniedException
{
int nId = Integer.parseInt( request.getParameter( PARAMETER_SECURITY_HEADER_ID ) );
SecurityHeader securityHeader = SecurityHeaderHome.findByPrimaryKey( nId );
String strErrors = processModifyFormData( request, securityHeader );
if ( strErrors != null )
{
return AdminMessageService.getMessageUrl( request, strErrors, AdminMessage.TYPE_STOP );
}
if ( !SecurityTokenService.getInstance( ).validate( request, TEMPLATE_MODIFY_SECURITYHEADER ) )
{
throw new AccessDeniedException( ERROR_INVALID_TOKEN );
}
getSecurityHeaderService( ).update( securityHeader );
return getHomeUrl( request );
}
/**
* Processes Modify Form Data.
*
* @param request
* The HTTP request
* @param securityheader
* The security header
* @return An Error message or null if no error
*/
private String processModifyFormData( HttpServletRequest request, SecurityHeader securityHeader )
{
securityHeader.setName( request.getParameter( PARAMETER_NAME ) );
securityHeader.setValue( request.getParameter( PARAMETER_VALUE ) );
securityHeader.setDescription( request.getParameter( PARAMETER_DESCRIPTION ) );
securityHeader.setType( request.getParameter( PARAMETER_TYPE ) );
securityHeader.setPageCategory( getPageCategory( request.getParameter( PARAMETER_PAGE_CATEGORY ), securityHeader.getType( ) ) );
String strErrors = processCommonControls( securityHeader );
if( strErrors != null )
{
return strErrors;
}
if( !isSecurityHeaderToModifyUnique( securityHeader, securityHeader.getName( ), securityHeader.getType( ), securityHeader.getPageCategory( ) ) )
{
return MESSAGE_HEADER_ALREADY_EXIST;
}
return null;
}
/**
* Returns security header page category from request if security header type is page.
* If type is REST api, page category is not relevant and should not be filled so the method returns null.
*
* @param strType
* @param strPageCategoryFromRequest
* @return
*/
private String getPageCategory( String strPageCategoryFromRequest, String strType )
{
if( SecurityHeaderType.PAGE.getCode( ).equals( strType ) )
{
return strPageCategoryFromRequest ;
}
return null;
}
/**
* Checks if the security header to modify will be always unique after modifications are applied to prevent duplicates.
* The criteria used to determine if a security header is unique are name, type and page category (in case of security header type is equals to page).
*
* @param securityHeaderToModify
* The security header to modify
* @param strName
* The name to set
* @param strType
* The type to set
* @param strPageCategory
* The page category to set
* @return true if the security header after modification will be still unique, false otherwise
*/
private boolean isSecurityHeaderToModifyUnique( SecurityHeader securityHeaderToModify, String strName, String strType, String strPageCategory )
{
for( SecurityHeader securityHeader : getSecurityHeaderService( ).find( strName, strType, strPageCategory ) )
{
if( securityHeaderToModify.getId( ) != securityHeader.getId( ) )
{
return false;
}
}
return true;
}
/**
* Returns the page of confirmation for deleting a security header.
*
* @param request
* The Http Request
* @return the confirmation url
*/
public String getConfirmRemoveSecurityHeader( HttpServletRequest request )
{
Map<String, String> parameters = new HashMap<>( );
parameters.put( PARAMETER_SECURITY_HEADER_ID, request.getParameter( PARAMETER_SECURITY_HEADER_ID ) );
parameters.put( SecurityTokenService.PARAMETER_TOKEN, SecurityTokenService.getInstance( ).getToken( request, JSP_REMOVE_SECURITY_HEADERS ) );
return AdminMessageService.getMessageUrl( request, MESSAGE_CONFIRM_REMOVE, JSP_REMOVE_SECURITY_HEADERS, AdminMessage.TYPE_CONFIRMATION, parameters );
}
/**
* Processes the data capture form for removing a security header.
*
* @param request
* The HTTP Request
* @return The Jsp URL of the process result
* @throws AccessDeniedException
* if the security token is invalid
*/
public String doRemoveSecurityHeader( HttpServletRequest request ) throws AccessDeniedException
{
if ( !SecurityTokenService.getInstance( ).validate( request, JSP_REMOVE_SECURITY_HEADERS ) )
{
throw new AccessDeniedException( ERROR_INVALID_TOKEN );
}
String strId = request.getParameter( PARAMETER_SECURITY_HEADER_ID );
getSecurityHeaderService( ).remove( Integer.parseInt( strId ) );
return JSP_MANAGE_SECURITY_HEADERS;
}
/**
* Processes the security headers actions (enable and disable).
*
* @param request
* The HTTP request
* @return The forward URL
* @throws AccessDeniedException
* if the security token is invalid
*/
public String doSecurityHeaderAction( HttpServletRequest request ) throws AccessDeniedException
{
String strAction = request.getParameter( PARAMETER_ACTION );
int nId = Integer.parseInt( request.getParameter( PARAMETER_SECURITY_HEADER_ID ) );
if ( !SecurityTokenService.getInstance( ).validate( request, TEMPLATE_MANAGE_SECURITY_HEADERS ) )
{
throw new AccessDeniedException( ERROR_INVALID_TOKEN );
}
switch( strAction )
{
case ACTION_ENABLE:
getSecurityHeaderService( ).enable( nId );
break;
case ACTION_DISABLE:
getSecurityHeaderService( ).disable( nId );
break;
default:
AppLogService.error( "Unknown security header action : {}", strAction );
}
return getHomeUrl( request );
}
/**
* Returns the security header service.
*
* @return the security header service
*/
private SecurityHeaderService getSecurityHeaderService( )
{
return SpringContextService.getBean( "securityHeaderService" );
}
}