IdentityAttributeValidator.java
/*
* Copyright (c) 2002-2024, 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.plugins.identitystore.v3.web.request.validator;
import fr.paris.lutece.plugins.geocodes.business.City;
import fr.paris.lutece.plugins.geocodes.business.Country;
import fr.paris.lutece.plugins.geocodes.service.GeoCodesService;
import fr.paris.lutece.plugins.identitystore.business.attribute.AttributeKey;
import fr.paris.lutece.plugins.identitystore.business.referentiel.RefAttributeCertificationLevel;
import fr.paris.lutece.plugins.identitystore.business.referentiel.RefAttributeCertificationLevelHome;
import fr.paris.lutece.plugins.identitystore.cache.IdentityAttributeValidationCache;
import fr.paris.lutece.plugins.identitystore.service.attribute.IdentityAttributeService;
import fr.paris.lutece.plugins.identitystore.service.contract.AttributeCertificationDefinitionService;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeChangeStatus;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeDto;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeStatus;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.IdentityDto;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.IdentityChangeRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.util.Constants;
import fr.paris.lutece.plugins.identitystore.web.exception.ClientAuthorizationException;
import fr.paris.lutece.plugins.identitystore.web.exception.RequestContentFormattingException;
import fr.paris.lutece.plugins.identitystore.web.exception.RequestFormatException;
import fr.paris.lutece.plugins.identitystore.web.exception.ResourceNotFoundException;
import fr.paris.lutece.portal.business.datastore.DataEntity;
import fr.paris.lutece.portal.business.datastore.DataEntityHome;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Service class used to validate attribute values in requests
*/
public class IdentityAttributeValidator
{
private static final String PIVOT_CERTIF_LEVEL_THRESHOLD = "identitystore.identity.attribute.update.pivot.certif.level.threshold";
private static final String IDENTITY_CONNECTED_ALLOWED_ATTRIBUTES_MODIFICATION = "identitystore.identity.connected.allowed.attributes.modification";
private static final String PROPERTY_EMAIL_FORBIDDEN_DOMAINS = AppPropertiesService.getProperty("identitystore.identity.attribute.email.forbidden_domains");
private final IdentityAttributeValidationCache _cache = SpringContextService.getBean( "identitystore.identityAttributeValidationCache" );
private final int pivotCertificationLevelThreshold = AppPropertiesService
.getPropertyInt( "identitystore.identity.attribute.pivot.certification.level.threshold", 400 );
private static IdentityAttributeValidator _instance;
public static IdentityAttributeValidator instance( )
{
if ( _instance == null )
{
_instance = new IdentityAttributeValidator( );
_instance._cache.refresh( );
}
return _instance;
}
/**
* Validate if the given attribute value matches the regexp defined for the given attribute key
* @param key
* @param value
* @return
* @throws ResourceNotFoundException
*/
public boolean validateAttribute( final String key, final String value) throws ResourceNotFoundException {
return _cache.get( key ).matcher( value ).matches( );
}
/**
* Checks if the request identity contains valid attributes that exist in the referential
*
* @param identity
* the identity
* @throws RequestFormatException
*/
public void checkAttributeExistence( final IdentityDto identity ) throws RequestFormatException
{
this.checkAttributeExistence(identity.getAttributes().stream().map(AttributeDto::getKey).collect(Collectors.toList()));
}
/**
* Checks if the request attribute keys correspond to valid attributes that exist in the referential
*
* @param attributeKeys
* the attribute keys
* @throws RequestFormatException
*/
public void checkAttributeExistence( final Collection<String> attributeKeys ) throws RequestFormatException
{
for ( final String attributeKey : attributeKeys )
{
try
{
IdentityAttributeService.instance( ).getAttributeKey( attributeKey );
}
catch( final ResourceNotFoundException e )
{
throw new RequestFormatException( "Attribute doesn't exist : " + attributeKey, Constants.PROPERTY_REST_ERROR_UNKNOWN_ATTRIBUTE_KEY );
}
}
}
/**
* Validates all attribute values stored in the provided identity, according to each attribute validation regex.
*
* @param identity
* the identity
* @throws RequestContentFormattingException
*/
public void validateIdentityAttributeValues( final IdentityDto identity ) throws RequestContentFormattingException
{
final List<AttributeStatus> attrStatusList = new ArrayList<>( );
if ( identity != null )
{
for ( final AttributeDto attribute : identity.getAttributes( ) )
{
if ( StringUtils.isNotBlank( attribute.getValue( ) ) )
{
try
{
final Pattern validationPattern = _cache.get( attribute.getKey( ) );
if ( validationPattern != null )
{
if ( !validationPattern.matcher( attribute.getValue( ) ).matches( ) )
{
attrStatusList.add( this.buildAttributeValueValidationErrorStatus( attribute.getKey( ) ) );
}
}
}
catch( final ResourceNotFoundException e )
{
// If attribute doesn't exist, do nothing.
}
}
// In particular case of an email, check if the domain is not in the banned domains property of the core datastore
if( StringUtils.equals(attribute.getKey( ), Constants.PARAM_EMAIL ) || ( StringUtils.equals(attribute.getKey(), Constants.PARAM_LOGIN) && attribute.getValue( ).contains( "@" ) ) )
{
final DataEntity dataEntity = DataEntityHome.findByPrimaryKey( PROPERTY_EMAIL_FORBIDDEN_DOMAINS );
if( dataEntity != null && Arrays.stream( dataEntity.getValue( ).split( ";" ) ).anyMatch( domain -> attribute.getValue( ).contains( domain ) ) )
{
final AttributeStatus attributeStatus = new AttributeStatus( );
attributeStatus.setKey( attribute.getKey() );
attributeStatus.setStatus( AttributeChangeStatus.INVALID_VALUE );
attributeStatus.setMessage( "Forbidden domain" );
attributeStatus.setMessageKey( Constants.PROPERTY_REST_ERROR_FORBIDDEN_EMAIL_DOMAIN );
attrStatusList.add(attributeStatus);
}
}
}
}
if ( !attrStatusList.isEmpty( ) )
{
final RequestContentFormattingException exception = new RequestContentFormattingException(
"Some attribute values are not passing validation. Please check in the attribute statuses for details.",
Constants.PROPERTY_REST_ERROR_FAIL_ATTRIBUTE_VALIDATION );
exception.getResponse( ).getStatus( ).setAttributeStatuses( attrStatusList );
throw exception;
}
}
/**
* Builds an attribute status for invalid value.
*
* @param attrStrKey
* the attribute key
* @return the status
*/
private AttributeStatus buildAttributeValueValidationErrorStatus( final String attrStrKey ) throws ResourceNotFoundException
{
final AttributeKey attributeKey = IdentityAttributeService.instance( ).getAttributeKey( attrStrKey );
final AttributeStatus attributeStatus = new AttributeStatus( );
attributeStatus.setKey( attrStrKey );
attributeStatus.setStatus( AttributeChangeStatus.INVALID_VALUE );
attributeStatus.setMessage( attributeKey.getValidationErrorMessage( ) );
attributeStatus.setMessageKey( attributeKey.getValidationErrorMessageKey( ) );
return attributeStatus;
}
/**
* Validates the integrity of pivot attributes stored in the provided identity.<br/>
* If a pivot attribute is present and certified with a process with a greater or equal level to the defined process in configuration, then ALL the pivot
* attributes need to be present and certified with the same certification process.<br/>
* If the birthcountry code attribute is set and doesn't correspond to FRANCE, then the birthplace code attribute is allowed to be missing.<br/>
* If the above rules are not fullfiled, the response status is put to FAILURE.
*
* @param existingIdentityDto
* the existing identity in case of update, <code>null</code> otherwise
* @param identity
* the identity
* @param geocodesCheck
* if checks geocode or not
* @throws RequestFormatException
*/
public void validatePivotAttributesIntegrity( final IdentityDto existingIdentityDto, final IdentityDto identity, boolean geocodesCheck )
throws RequestFormatException
{
// get pivot attributes
final List<String> pivotKeys = IdentityAttributeService.instance( ).getPivotAttributeKeys( ).stream( ).map( AttributeKey::getKeyName )
.collect( Collectors.toList( ) );
// get attributes to update, and add certification levels
final Map<String, AttributeDto> pivotUpdatedAttrs = identity.getAttributes( ).stream( ).filter( a -> pivotKeys.contains( a.getKey( ) ) )
.peek( a -> a.setCertificationLevel( AttributeCertificationDefinitionService.instance( ).getLevelAsInteger( a.getCertifier( ), a.getKey( ) ) ) )
.collect( Collectors.toMap( AttributeDto::getKey, Function.identity( ) ) );
// If the request does not contains at least one pivot attribute, we skip this validation rule
if ( pivotUpdatedAttrs.isEmpty( ) )
{
return;
}
// in case of update, we get the existing attributes for the check
final Map<String, AttributeDto> pivotExistingAttrs = new HashMap<>( );
if ( existingIdentityDto != null )
{
pivotExistingAttrs.putAll( existingIdentityDto.getAttributes( ).stream( ).filter( a -> pivotKeys.contains( a.getKey( ) ) )
.collect( Collectors.toMap( AttributeDto::getKey, Function.identity( ) ) ) );
}
// *** Geocode checks ***
// get birth date for Geocode checks
final List<AttributeStatus> geocodeStatuses = new ArrayList<>( );
Date birthdate = null;
AttributeDto birthdateAttr = pivotUpdatedAttrs.get( Constants.PARAM_BIRTH_DATE );
if ( birthdateAttr == null && existingIdentityDto != null )
{
birthdateAttr = pivotExistingAttrs.get( Constants.PARAM_BIRTH_DATE );
}
try
{
if ( birthdateAttr != null )
{
birthdate = DateUtils.parseDate( birthdateAttr.getValue( ), "dd/MM/yyyy" );
}
}
catch( final ParseException e )
{
final AttributeStatus birthplaceCodeStatus = new AttributeStatus( );
birthplaceCodeStatus.setKey( Constants.PARAM_BIRTH_DATE );
birthplaceCodeStatus.setStatus( AttributeChangeStatus.INVALID_VALUE );
birthplaceCodeStatus.setMessageKey( Constants.PROPERTY_ATTRIBUTE_STATUS_NOT_UPDATED );
geocodeStatuses.add( birthplaceCodeStatus );
}
if ( birthdate == null )
{
pivotUpdatedAttrs.remove( Constants.PARAM_BIRTH_DATE );
// birthdate is mandatory for birthplace and birthcountry codes
// we won't consider those values either (if present)
pivotUpdatedAttrs.remove( Constants.PARAM_BIRTH_PLACE_CODE );
pivotUpdatedAttrs.remove( Constants.PARAM_BIRTH_COUNTRY_CODE );
}
// Birthplace codes checks
// If invalid, we consider them as missing
if ( geocodesCheck && pivotUpdatedAttrs.containsKey( Constants.PARAM_BIRTH_PLACE_CODE ) && birthdate != null )
{
final AttributeDto birthPlaceCodeAttr = pivotUpdatedAttrs.get( Constants.PARAM_BIRTH_PLACE_CODE );
if ( StringUtils.isNotBlank( birthPlaceCodeAttr.getValue( ) ) )
{
final Optional<City> city = GeoCodesService.getInstance( ).getCityByDateAndCode( birthdate, birthPlaceCodeAttr.getValue( ) );
if ( city == null || !city.isPresent( ) )
{
pivotUpdatedAttrs.remove( Constants.PARAM_BIRTH_PLACE_CODE );
final AttributeStatus birthplaceCodeStatus = new AttributeStatus( );
birthplaceCodeStatus.setKey( Constants.PARAM_BIRTH_PLACE_CODE );
birthplaceCodeStatus.setStatus( AttributeChangeStatus.UNKNOWN_GEOCODES_CODE );
birthplaceCodeStatus.setMessageKey( Constants.PROPERTY_ATTRIBUTE_STATUS_VALIDATION_ERROR_UNKNOWN_GEOCODES_CODE );
geocodeStatuses.add( birthplaceCodeStatus );
}
}
}
if ( geocodesCheck && pivotUpdatedAttrs.containsKey( Constants.PARAM_BIRTH_COUNTRY_CODE ) && birthdate != null )
{
final AttributeDto birthcountryCodeAttr = pivotUpdatedAttrs.get( Constants.PARAM_BIRTH_COUNTRY_CODE );
if ( StringUtils.isNotBlank( birthcountryCodeAttr.getValue( ) ) )
{
// TODO : use GeoCodesService.getInstance( ).getCountryByDateAndCode() when available ...
final Optional<Country> country = GeoCodesService.getInstance( ).getCountryByCode( birthcountryCodeAttr.getValue( ) );
if ( country == null || !country.isPresent( ) )
{
pivotUpdatedAttrs.remove( Constants.PARAM_BIRTH_COUNTRY_CODE );
final AttributeStatus birthcountryCodeStatus = new AttributeStatus( );
birthcountryCodeStatus.setKey( Constants.PARAM_BIRTH_COUNTRY_CODE );
birthcountryCodeStatus.setStatus( AttributeChangeStatus.UNKNOWN_GEOCODES_CODE );
birthcountryCodeStatus.setMessageKey( Constants.PROPERTY_ATTRIBUTE_STATUS_VALIDATION_ERROR_UNKNOWN_GEOCODES_CODE );
geocodeStatuses.add( birthcountryCodeStatus );
}
}
}
// ** consolidate targeted list of pivot attributes with new or updated valid attributes, and existing attributes **
final Map<String, AttributeDto> pivotTargetAttrs = new HashMap<>( );
pivotTargetAttrs.putAll( pivotUpdatedAttrs );
// in case of update, we include the existing attributes for the check
if ( existingIdentityDto != null )
{
// Get the targeted pivot attributes from updated pivot attributes
// and pivot attributes that exist and are not present in request.
// If there is an existing attribute with a higher certification level, we ignore the request attribute
pivotTargetAttrs.putAll( pivotExistingAttrs.keySet( ).stream( )
.filter( a -> ( !pivotUpdatedAttrs.containsKey( a )
|| pivotUpdatedAttrs.get( a ).getCertificationLevel( ) < pivotExistingAttrs.get( a ).getCertificationLevel( ) ) )
.collect( Collectors.toMap( Function.identity( ), pivotExistingAttrs::get ) ) );
}
// if the birth country is not the main Geocode country, the birth place code is ignored
if ( pivotTargetAttrs.containsKey( Constants.PARAM_BIRTH_COUNTRY_CODE )
&& !pivotTargetAttrs.get( Constants.PARAM_BIRTH_COUNTRY_CODE ).getValue( ).equals( Constants.GEOCODE_MAIN_COUNTRY_CODE ) )
{
pivotKeys.remove( Constants.PARAM_BIRTH_PLACE_CODE );
pivotUpdatedAttrs.remove( Constants.PARAM_BIRTH_PLACE_CODE );
pivotTargetAttrs.remove( Constants.PARAM_BIRTH_PLACE_CODE );
}
// ** Main checks on target Attributes **
// get highest certification level for pivot attributes (as reference)
final AttributeDto highestCertifiedPivot = pivotTargetAttrs.values( ).stream( ).max( Comparator.comparing( AttributeDto::getCertificationLevel ) )
.orElse( null );
// if the highest certification level is below the treshold, we wont carry out the check
if ( highestCertifiedPivot.getCertificationLevel( ) < pivotCertificationLevelThreshold )
{
return;
}
// check that we have the 6 pivot attributes (or 5 if birth country is not the geocode main country)
if ( !( ( pivotTargetAttrs.size( ) == pivotKeys.size( ) ) || ( pivotTargetAttrs.size( ) >= pivotKeys.size( )
&& !Constants.GEOCODE_MAIN_COUNTRY_CODE.equals( pivotTargetAttrs.get( Constants.PARAM_BIRTH_COUNTRY_CODE ).getValue( ) ) ) ) )
{
final RequestFormatException exception = new RequestFormatException(
"Above level " + pivotCertificationLevelThreshold + ", all pivot attributes must be present and have the same certification level.",
Constants.PROPERTY_REST_ERROR_IDENTITY_ALL_PIVOT_ATTRIBUTE_SAME_CERTIFICATION );
exception.getResponse( ).getStatus( ).setAttributeStatuses( geocodeStatuses );
throw exception;
}
// we check that each pivot attribute has the same certification level
if ( pivotTargetAttrs.values( ).stream( ).anyMatch( a -> !a.getCertifier( ).equals( highestCertifiedPivot.getCertifier( ) ) ) )
{
final RequestFormatException exception = new RequestFormatException(
"All pivot attributes must be set and certified with the '" + highestCertifiedPivot.getCertifier( ) + "' certifier",
Constants.PROPERTY_REST_ERROR_IDENTITY_ALL_PIVOT_ATTRIBUTE_SAME_CERTIFICATION );
exception.getResponse( ).getStatus( ).setAttributeStatuses( geocodeStatuses );
throw exception;
}
// Check of empty values
// (deletion of pivot attributes is not allowed above the pivotCertificationLevelThreshold)
if ( pivotTargetAttrs.values( ).stream( ).anyMatch( a -> StringUtils.isBlank( a.getValue( ) ) ) )
{
final RequestFormatException exception = new RequestFormatException( "Deleting pivot attribute is forbidden for this identity.",
Constants.PROPERTY_REST_ERROR_IDENTITY_FORBIDDEN_PIVOT_ATTRIBUTE_DELETION );
exception.getResponse( ).getStatus( ).setAttributeStatuses( geocodeStatuses );
throw exception;
}
}
/**
* Makes a bunch of checks regarding the validity of this update or merge request on this connected identity.
* <ul>
* <li>Authorise update on attributes in property only</li>
* <li>For new attributes, certification level must be > 100 (better than self-declare)</li>
* <li>For existing attributes, certification level must be >= than the existing level</li>
* <li>If one "PIVOT" attribute is certified at a certain level N (conf) :
* <ul>
* <li>All "PIVOT" attributes must be set</li>
* <li>All "PIVOT" attributes must be certified with level greater or equal to N</li>
* </ul>
* </li>
* </ul>
*
* @param requestAttributes
* the attributes in the update or merge request
* @param existingIdentityToUpdate
* the existing identity
*/
public void checkConnectedIdentityUpdate( final List<AttributeDto> requestAttributes, final IdentityDto existingIdentityToUpdate )
throws ClientAuthorizationException
{
final Map<String, AttributeDto> existingAttributes = existingIdentityToUpdate.getAttributes( ).stream( )
.collect( Collectors.toMap( AttributeDto::getKey, Function.identity( ) ) );
/* Récupération des attributs déja existants ou non */
final Map<Boolean, List<AttributeDto>> sortedAttributes = requestAttributes.stream( )
.collect( Collectors.partitioningBy( a -> existingAttributes.containsKey( a.getKey( ) ) ) );
final List<AttributeDto> existingWritableAttributes = CollectionUtils.isNotEmpty( sortedAttributes.get( true ) ) ? sortedAttributes.get( true )
: new ArrayList<>( );
final List<AttributeDto> newWritableAttributes = CollectionUtils.isNotEmpty( sortedAttributes.get( false ) ) ? sortedAttributes.get( false )
: new ArrayList<>( );
final Map<String, AttributeKey> allAttributesByKey = IdentityAttributeService.instance( ).getAllAtributeKeys( ).stream( )
.collect( Collectors.toMap( AttributeKey::getKeyName, Function.identity( ) ) );
// LUT-28919 - Authorize update on attributes in property only
final List<String> allowedAttrKeys = Arrays.asList(AppPropertiesService.getProperty(IDENTITY_CONNECTED_ALLOWED_ATTRIBUTES_MODIFICATION).split(","));
final boolean requestOnAllowedAttributes = requestAttributes.stream().allMatch(a -> allowedAttrKeys.contains(a.getKey()));
if ( !requestOnAllowedAttributes )
{
throw new ClientAuthorizationException( "Identity is connected, updating attributes is restricted. Allowed attributes to modify are : " + allowedAttrKeys,
Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_RESTRICTED_ATTRIBUTE_UPDATE );
}
// - For new attributes, certification level must be > 100 (better than self-declare)
final boolean newAttrSelfDeclare = newWritableAttributes.stream( )
.map( a -> RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( a.getCertifier( ), a.getKey( ) ) )
.anyMatch( c -> Integer.parseInt( c.getRefCertificationLevel( ).getLevel( ) ) <= 100 );
if ( newAttrSelfDeclare )
{
throw new ClientAuthorizationException( "Identity is connected, adding attributes with self-declarative certification level is forbidden.",
Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_SELF_DECLARE );
}
// - For existing attributes, certification level must be >= than the existing level
final boolean lesserWantedLvl = existingWritableAttributes.stream( )
.map( a -> RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( a.getCertifier( ), a.getKey( ) ) ).anyMatch( wantedCertif -> {
final int wantedLvl = Integer.parseInt( wantedCertif.getRefCertificationLevel( ).getLevel( ) );
final AttributeDto existingAttr = existingAttributes.get( wantedCertif.getAttributeKey( ).getKeyName( ) );
final RefAttributeCertificationLevel existingCertif = RefAttributeCertificationLevelHome
.findByProcessusAndAttributeKeyName( existingAttr.getCertifier( ), existingAttr.getKey( ) );
final int existingLvl = Integer.parseInt( existingCertif.getRefCertificationLevel( ).getLevel( ) );
return wantedLvl < existingLvl;
} );
if ( lesserWantedLvl )
{
throw new ClientAuthorizationException( "Identity is connected, updating existing attributes with lesser certification level is forbidden.",
Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_UPDATE_LESSER_CERTIFICATION );
}
// - If one "PIVOT" attribute is certified at a certain level N (conf), all "PIVOT" attributes must be set and certified with level >= N.
final int threshold = AppPropertiesService.getPropertyInt( PIVOT_CERTIF_LEVEL_THRESHOLD, 400 );
// get all pivot attributes from database
final List<String> pivotAttributeKeys = allAttributesByKey.values( ).stream( ).filter( AttributeKey::getPivot ).map( AttributeKey::getKeyName )
.collect( Collectors.toList( ) );
final boolean breakingThreshold = existingIdentityToUpdate.getAttributes( ).stream( ).filter( a -> allAttributesByKey.get( a.getKey( ) ).getPivot( ) )
.map( a -> RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( a.getCertifier( ), a.getKey( ) ) )
.anyMatch( c -> Integer.parseInt( c.getRefCertificationLevel( ).getLevel( ) ) >= threshold )
|| requestAttributes.stream( )
.filter(a -> pivotAttributeKeys.contains(a.getKey()))
.map( a -> RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( a.getCertifier( ), a.getKey( ) ) )
.anyMatch( c -> Integer.parseInt( c.getRefCertificationLevel( ).getLevel( ) ) >= threshold );
if ( breakingThreshold )
{
// if any pivot is missing from request + existing -> unauthorized
final Collection<String> unionOfExistingAndRequestedPivotKeys = CollectionUtils
.union( requestAttributes.stream( ).map( AttributeDto::getKey ).filter( pivotAttributeKeys::contains ).collect( Collectors.toSet( ) ), existingIdentityToUpdate.getAttributes( )
.stream( ).map( AttributeDto::getKey ).filter( key -> allAttributesByKey.get( key ).getPivot( ) ).collect( Collectors.toSet( ) ) );
if ( !CollectionUtils.isEqualCollection( pivotAttributeKeys, unionOfExistingAndRequestedPivotKeys ) )
{
throw new ClientAuthorizationException(
"Identity is connected, and at least one 'pivot' attribute is, or has been requested to be, certified above level " + threshold
+ ". In that case, all 'pivot' attributes must be set, and certified with level greater or equal to " + threshold + ".",
Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_PIVOT_CERTIFICATION_UNDER_THRESHOLD );
}
// if any has level lesser than threshold -> unauthorized
final boolean lesserThanThreshold = pivotAttributeKeys.stream( ).map( key -> {
final AttributeDto requested = requestAttributes.stream( ).filter( a -> a.getKey( ).equals( key ) ).findFirst( ).orElse( null );
final AttributeDto existing = existingAttributes.get( key );
int requestedLvl = 0;
int existingLvl = 0;
if ( requested != null )
{
requestedLvl = Integer.parseInt( RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( requested.getCertifier( ), key )
.getRefCertificationLevel( ).getLevel( ) );
}
if ( existing != null )
{
existingLvl = Integer.parseInt( RefAttributeCertificationLevelHome.findByProcessusAndAttributeKeyName( existing.getCertifier( ), key )
.getRefCertificationLevel( ).getLevel( ) );
}
return Math.max( requestedLvl, existingLvl );
} ).anyMatch( lvl -> lvl < threshold );
if ( lesserThanThreshold )
{
throw new ClientAuthorizationException(
"Identity is connected, and at least one 'pivot' attribute is, or has been requested to be, certified above level " + threshold
+ ". In that case, all 'pivot' attributes must be set, and certified with level greater or equal to " + threshold + ".",
Constants.PROPERTY_REST_ERROR_CONNECTED_IDENTITY_FORBIDDEN_PIVOT_CERTIFICATION_UNDER_THRESHOLD );
}
}
}
}