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" );
    }
}