IdentityService.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.service.identity;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import fr.paris.lutece.plugins.identitystore.business.application.ClientApplication;
import fr.paris.lutece.plugins.identitystore.business.attribute.AttributeKey;
import fr.paris.lutece.plugins.identitystore.business.contract.ServiceContract;
import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.ExcludedIdentities;
import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.SuspiciousIdentity;
import fr.paris.lutece.plugins.identitystore.business.duplicates.suspicions.SuspiciousIdentityHome;
import fr.paris.lutece.plugins.identitystore.business.identity.Identity;
import fr.paris.lutece.plugins.identitystore.business.identity.IdentityAttribute;
import fr.paris.lutece.plugins.identitystore.business.identity.IdentityHome;
import fr.paris.lutece.plugins.identitystore.business.rules.duplicate.DuplicateRule;
import fr.paris.lutece.plugins.identitystore.cache.IdentityDtoCache;
import fr.paris.lutece.plugins.identitystore.service.attribute.IdentityAttributeService;
import fr.paris.lutece.plugins.identitystore.service.contract.AttributeCertificationDefinitionService;
import fr.paris.lutece.plugins.identitystore.service.listeners.IdentityStoreNotifyListenerService;
import fr.paris.lutece.plugins.identitystore.service.search.ISearchIdentityService;
import fr.paris.lutece.plugins.identitystore.service.user.InternalUserService;
import fr.paris.lutece.plugins.identitystore.utils.Batch;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.DtoConverter;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeChangeStatus;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AttributeChangeStatusType;
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.AuthorType;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.IdentityDto;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.Page;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.QualityDefinition;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.RequestAuthor;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.ResponseStatusType;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.IdentityChangeRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.crud.UpdatedIdentityDto;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.history.AttributeChangeType;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.history.IdentityChangeType;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.merge.IdentityMergeRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.IdentitySearchRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.QualifiedIdentitySearchResult;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.SearchAttribute;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.search.UpdatedIdentitySearchRequest;
import fr.paris.lutece.plugins.identitystore.v3.web.rs.util.Constants;
import fr.paris.lutece.plugins.identitystore.web.exception.IdentityStoreException;
import fr.paris.lutece.plugins.identitystore.web.exception.RequestFormatException;
import fr.paris.lutece.plugins.identitystore.web.exception.ResourceNotFoundException;
import fr.paris.lutece.plugins.notificationstore.v1.web.service.NotificationStoreService;
import fr.paris.lutece.portal.service.security.AccessLogService;
import fr.paris.lutece.portal.service.security.AccessLoggerConstants;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.util.http.SecurityUtil;
import fr.paris.lutece.util.sql.TransactionManager;
public class IdentityService
{
// Conf
private static final String PIVOT_UNCERTIF_LEVEL_THRESHOLD = "identitystore.identity.uncertify.attribute.pivot.level.threshold";
// EVENTS FOR ACCESS LOGGING
public static final String CREATE_IDENTITY_EVENT_CODE = "CREATE_IDENTITY";
public static final String UPDATE_IDENTITY_EVENT_CODE = "UPDATE_IDENTITY";
public static final String DECERTIFY_IDENTITY_EVENT_CODE = "DECERTIFY_IDENTITY";
public static final String GET_IDENTITY_EVENT_CODE = "GET_IDENTITY";
public static final String SEARCH_IDENTITY_EVENT_CODE = "SEARCH_IDENTITY";
public static final String DELETE_IDENTITY_EVENT_CODE = "DELETE_IDENTITY";
public static final String CONSOLIDATE_IDENTITY_EVENT_CODE = "CONSOLIDATE_IDENTITY";
public static final String MERGE_IDENTITY_EVENT_CODE = "MERGE_IDENTITY";
public static final String CANCEL_MERGE_IDENTITY_EVENT_CODE = "CANCEL_MERGE_IDENTITY";
public static final String CANCEL_CONSOLIDATE_IDENTITY_EVENT_CODE = "CANCEL_CONSOLIDATE_IDENTITY";
public static final String SPECIFIC_ORIGIN = "BO";
// PROPERTIES
private static final String PROPERTY_MAX_RESULT_UPDATED_IDENTITY_SEARCH = "identitystore.identity.updated.size.limit";
// SERVICES
private final IdentityStoreNotifyListenerService _identityStoreNotifyListenerService = IdentityStoreNotifyListenerService.instance( );
private final IdentityAttributeService _identityAttributeService = IdentityAttributeService.instance( );
private final InternalUserService _internalUserService = InternalUserService.getInstance( );
private final ISearchIdentityService _elasticSearchIdentityService = SpringContextService.getBean( "identitystore.searchIdentityService.elasticsearch" );
private final NotificationStoreService _notificationStoreService = SpringContextService.getBean( "notificationStore.notificationStoreService" );
// CACHE
private final IdentityDtoCache _identityDtoCache = SpringContextService.getBean( "identitystore.identityDtoCache" );
private static IdentityService _instance;
public static IdentityService instance( )
{
if ( _instance == null )
{
_instance = new IdentityService( );
}
return _instance;
}
/**
* Creates a new {@link Identity} according to the given {@link IdentityChangeRequest}
*
* @param request
* the {@link IdentityChangeRequest} holding the parameters of the identity change request
* @param author
* the author of the request
* @param serviceContract
* service contract of the client requesting the change
* @param formatStatuses
* the attribute formatting statuses (only for history purposes)
* @return the created {@link Identity} along with a list of the {@link AttributeStatus}
* @throws IdentityStoreException
* in case of error
*/
public Pair<Identity, List<AttributeStatus>> create( final IdentityChangeRequest request, final RequestAuthor author, final ServiceContract serviceContract,
final List<AttributeStatus> formatStatuses ) throws IdentityStoreException
{
final String clientCode = serviceContract.getClientCode( );
final Identity identity = new Identity( );
final List<AttributeStatus> attrStatusList = new ArrayList<>( );
TransactionManager.beginTransaction( null );
try
{
identity.setMonParisActive( request.getIdentity( ).isMonParisActive( ) );
if ( StringUtils.isNotEmpty( request.getIdentity( ).getConnectionId( ) ) )
{
identity.setConnectionId( request.getIdentity( ).getConnectionId( ) );
}
IdentityHome.create( identity, serviceContract.getDataRetentionPeriodInMonths( ) );
for ( final AttributeDto attributeDto : request.getIdentity( ).getAttributes( ) )
{
final AttributeStatus attributeStatus = _identityAttributeService.createAttribute( attributeDto, identity, clientCode );
attrStatusList.add( attributeStatus );
}
TransactionManager.commitTransaction( null );
/* Historique des modifications */
for ( final AttributeStatus attributeStatus : attrStatusList )
{
_identityStoreNotifyListenerService.notifyListenersAttributeChange( AttributeChangeType.CREATE, identity, attributeStatus, author, clientCode );
}
/* Indexation et historique */
final boolean incompleteCreation = Stream.concat( attrStatusList.stream( ), formatStatuses.stream( ) )
.anyMatch( s -> s.getStatus( ).equals( AttributeChangeStatus.NOT_CREATED ) );
final ResponseStatusType statusType = incompleteCreation ? ResponseStatusType.INCOMPLETE_SUCCESS : ResponseStatusType.SUCCESS;
final String statusMessage = incompleteCreation ? "incomplete success" : "success";
_identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.CREATE, identity, statusType.name( ), statusMessage, author,
clientCode, new HashMap<>( ) );
AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_CREATE, CREATE_IDENTITY_EVENT_CODE,
_internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( identity.getCustomerId( ) ), SPECIFIC_ORIGIN );
return Pair.of( identity, attrStatusList );
}
catch( final Exception e )
{
TransactionManager.rollBack( null );
if ( e instanceof IdentityStoreException )
{
throw e;
}
throw new IdentityStoreException( e.getMessage( ), e, Constants.PROPERTY_REST_ERROR_DURING_TREATMENT );
}
}
/**
* Updates an existing {@link Identity} according to the given {@link IdentityChangeRequest} and following the given rules: <br>
* <ul>
* <li>The {@link Identity} must exist in te database. If not, NOT_FOUND status is returned in the execution response</li>
* <li>The {@link Identity} must not be merged or deleted. In case of merged/deleted identity, the update is not performed and the customer ID of the
* primary identity is returned in the execution response with a CONFLICT status</li>
* <li>If the {@link Identity} can be updated, its {@link IdentityAttribute} list is updated following the given rule:
* <ul>
* <li>If the {@link IdentityAttribute} exists, it is updated if the value is different, and if the process level given in the request is higher than the
* existing one. If the value cannot be updated, the NOT_UPDATED status, associated with the attribute key, is returned in the execution response.</li>
* <li>If the {@link IdentityAttribute} does not exist, it is created. The CREATED status, associated with the attribute key, is returned in the execution
* response.</li>
* <li>CUID and GUID attributes cannot be modified.</li>
* </ul>
* </li>
* </ul>
*
* @param customerId
* the id of the updated {@link Identity}
* @param request
* the {@link IdentityChangeRequest} holding the parameters of the identity change request
* @param author
* the author of the request
* @param serviceContract
* service contract of the client requesting the change
* @param formatStatuses
* the attribute formatting statuses (only for history purposes)
* @return the updated {@link Identity} along with the attribute statuses
* @throws IdentityStoreException
* in case of error
*/
public Pair<Identity, List<AttributeStatus>> update( final String customerId, final IdentityChangeRequest request, final RequestAuthor author,
final ServiceContract serviceContract, final List<AttributeStatus> formatStatuses ) throws IdentityStoreException
{
final String clientCode = serviceContract.getClientCode( );
final Identity identity = IdentityHome.findByCustomerId( customerId );
TransactionManager.beginTransaction( null );
try
{
final Map<String, String> metadata = new HashMap<>();
if ( !StringUtils.equalsIgnoreCase( identity.getConnectionId( ), request.getIdentity( ).getConnectionId( ) )
&& StringUtils.isNotEmpty( request.getIdentity( ).getConnectionId( ) ) )
{
metadata.put(Constants.METADATA_NEW_GUID, request.getIdentity( ).getConnectionId( ) );
metadata.put(Constants.METADATA_OLD_GUID, identity.getConnectionId( ) );
identity.setConnectionId( request.getIdentity( ).getConnectionId( ) );
IdentityHome.update( identity );
}
// => process update :
final List<AttributeStatus> attrStatusList = this.updateIdentity( identity, request.getIdentity( ), clientCode, metadata, false);
TransactionManager.commitTransaction( null );
/* Historique des modifications */
for ( final AttributeStatus attributeStatus : attrStatusList )
{
_identityStoreNotifyListenerService.notifyListenersAttributeChange( AttributeChangeType.UPDATE, identity, attributeStatus, author, clientCode );
}
/* Indexation et historique */
final boolean allAttrCreatedOrUpdated = Stream.concat( attrStatusList.stream( ), formatStatuses.stream( ) ).map( AttributeStatus::getStatus )
.allMatch( status -> status.getType( ) == AttributeChangeStatusType.SUCCESS );
final ResponseStatusType statusType = allAttrCreatedOrUpdated ? ResponseStatusType.SUCCESS : ResponseStatusType.INCOMPLETE_SUCCESS;
final String statusMessage = allAttrCreatedOrUpdated ? "success" : "incomplete success";
_identityStoreNotifyListenerService.notifyListenersIdentityChange(IdentityChangeType.UPDATE, identity, statusType.name( ), statusMessage, author,
clientCode, metadata);
AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, UPDATE_IDENTITY_EVENT_CODE,
_internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( identity.getCustomerId( ) ), SPECIFIC_ORIGIN );
return Pair.of( identity, attrStatusList );
}
catch( final Exception e )
{
TransactionManager.rollBack( null );
if ( e instanceof IdentityStoreException )
{
throw e;
}
throw new IdentityStoreException( e.getMessage( ), e, Constants.PROPERTY_REST_ERROR_DURING_TREATMENT );
}
}
/**
* Merges two existing {@link Identity} specified in the given {@link IdentityMergeRequest} and following the given rules: <br>
* <ul>
* <li>Both {@link Identity} must exist and not be merged or deleted in te database. If not, FAILURE status is returned in the execution response</li>
* <li>The {@link IdentityAttribute}(s) of the secondary {@link Identity} are processed, according to the list of keys specified in the
* {@link IdentityMergeRequest}.
* <ul>
* <li>If the {@link IdentityAttribute} is not present in the primary {@link Identity}, it is created.</li>
* <li>If the {@link IdentityAttribute} is present in the primary {@link Identity}, it is updated if the certification process is higher and the value is
* different.</li>
* </ul>
* </li>
* </ul>
*
* @param primaryIdentity the primary identity (master)
* @param secondaryIdentity the secondary identity (merged)
* @param identityForConsolidate the identityDto holding the attributes for consolidation
* @param duplicateRuleCode the duplicate rule code
* @param author
* the author of the request
* @param clientCode
* code of the {@link ClientApplication} requesting the change
* @param formatStatuses the format statuses (only for history)
* @return the merged {@link Identity}
* @throws IdentityStoreException
* in case of error
*/
public Pair<Identity, List<AttributeStatus>> merge( final Identity primaryIdentity, final Identity secondaryIdentity, final IdentityDto identityForConsolidate, final String duplicateRuleCode, final RequestAuthor author, final String clientCode,
final List<AttributeStatus> formatStatuses ) throws IdentityStoreException
{
TransactionManager.beginTransaction( null );
try
{
final List<AttributeStatus> attrStatusList = new ArrayList<>( );
final Map<String, String> primaryMetadata = new HashMap<>( );
Identity consolidatedIdentity = IdentityHome.findByCustomerId( primaryIdentity.getCustomerId( ) );
primaryIdentity.setId( consolidatedIdentity.getId() );
Identity mergedIdentity = IdentityHome.findByCustomerId( secondaryIdentity.getCustomerId( ) );
secondaryIdentity.setId( mergedIdentity.getId() );
boolean identityUpdateRequested = false;
// update expiration date if necessary
if ( consolidatedIdentity.getExpirationDate( ).before( mergedIdentity.getExpirationDate( ) ) )
{
primaryIdentity.setExpirationDate( mergedIdentity.getExpirationDate( ) );
identityUpdateRequested = true;
}
if ( identityForConsolidate != null && CollectionUtils.isNotEmpty( identityForConsolidate.getAttributes( ) ) )
{
// search and keep the original certifier client (and certification date) in merge request
identityForConsolidate.getAttributes ( ).stream ( )
.filter( attr -> secondaryIdentity.getAttributes( ).containsKey( attr.getKey( ) ) )
.forEach( attr -> {
String lastClientCode = secondaryIdentity.getAttributes( ).get( attr.getKey( ) ).getLastUpdateClientCode( );
attr.setLastUpdateClientCode ( lastClientCode );
});
// try to update
attrStatusList.addAll( this.updateIdentity( primaryIdentity, identityForConsolidate, clientCode, primaryMetadata, identityUpdateRequested, true ) );
}
// Mise à jour du lien entre les identités
secondaryIdentity.setMasterIdentityId( primaryIdentity.getId( ) );
IdentityHome.merge( secondaryIdentity );
// ré-affecter les notifications sur l'identité consolidée
_notificationStoreService.reassignNotifications( secondaryIdentity.getCustomerId(), primaryIdentity.getCustomerId() );
// commit
TransactionManager.commitTransaction( null );
/* Historique des modifications */
for ( AttributeStatus attributeStatus : attrStatusList )
{
_identityStoreNotifyListenerService.notifyListenersAttributeChange( AttributeChangeType.MERGE, primaryIdentity, attributeStatus, author,
clientCode );
}
/* Indexation et historique */
final boolean allAttrCreatedOrUpdated = Stream.concat( attrStatusList.stream( ), formatStatuses.stream( ) ).map( AttributeStatus::getStatus )
.allMatch( status -> status.getType( ) == AttributeChangeStatusType.SUCCESS );
final ResponseStatusType statusType = allAttrCreatedOrUpdated ? ResponseStatusType.SUCCESS : ResponseStatusType.INCOMPLETE_SUCCESS;
final String statusMessage = allAttrCreatedOrUpdated ? "success" : "incomplete success";
primaryMetadata.put( Constants.METADATA_MERGED_MASTER_IDENTITY_CUID, primaryIdentity.getCustomerId( ) );
primaryMetadata.put( Constants.METADATA_DUPLICATE_RULE_CODE, duplicateRuleCode );
_identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.MERGED, secondaryIdentity, statusType.name( ), statusMessage,
author, clientCode, primaryMetadata );
AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, CONSOLIDATE_IDENTITY_EVENT_CODE,
_internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( primaryIdentity.getCustomerId( ) ),
SPECIFIC_ORIGIN );
final Map<String, String> secondaryMetadata = new HashMap<>( );
secondaryMetadata.put( Constants.METADATA_MERGED_CHILD_IDENTITY_CUID, secondaryIdentity.getCustomerId( ) );
secondaryMetadata.put( Constants.METADATA_DUPLICATE_RULE_CODE, duplicateRuleCode );
_identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.CONSOLIDATED, primaryIdentity, statusType.name( ),
statusMessage, author, clientCode, secondaryMetadata );
AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, MERGE_IDENTITY_EVENT_CODE,
_internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( secondaryIdentity.getCustomerId( ) ),
SPECIFIC_ORIGIN );
return Pair.of( primaryIdentity, attrStatusList );
}
catch( IdentityStoreException e )
{
TransactionManager.rollBack( null );
throw e;
}
catch( Exception e )
{
TransactionManager.rollBack( null );
throw new IdentityStoreException( e.getMessage( ), e, Constants.PROPERTY_REST_ERROR_DURING_TREATMENT );
}
}
/**
* Detach a merged {@link Identity} from its master {@link Identity}
*
* @param request
* the unmerge request
* @param author
* the author of the request
* @param clientCode
* the client code of the calling application
*/
public Pair<Identity, List<AttributeStatus>> cancelMerge( final IdentityMergeRequest request, final RequestAuthor author, final String clientCode ) throws IdentityStoreException
{
final List<AttributeStatus> attrStatusList = new ArrayList<>( );
final Map<String, String> metadata = new HashMap<>( );
final Identity primaryIdentity = IdentityHome.findByCustomerId( request.getPrimaryCuid( ) );
Identity secondaryIdentity = IdentityHome.findByCustomerId( request.getSecondaryCuid( ) );
final IdentityDto dtoWithNewAttributes = request.getIdentity( );
TransactionManager.beginTransaction( null );
try
{
// Annulation du merge
IdentityHome.cancelMerge( secondaryIdentity );
// ajout des attributs minimum
if ( !dtoWithNewAttributes.getAttributes ( ).isEmpty ( ) )
{
attrStatusList.addAll( this.updateIdentity( secondaryIdentity, dtoWithNewAttributes, clientCode, metadata, false ) );
}
TransactionManager.commitTransaction( null );
}
catch( final Exception e )
{
TransactionManager.rollBack( null );
throw new IdentityStoreException( e.getMessage( ), e, Constants.PROPERTY_REST_ERROR_DURING_TREATMENT );
}
/* Notification listeners pour indexation/historique */
final Map<String, String> secondaryMetadata = new HashMap<>( );
secondaryMetadata.put( Constants.METADATA_UNMERGED_MASTER_CUID, primaryIdentity.getCustomerId( ) );
_identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.MERGE_CANCELLED, secondaryIdentity,
ResponseStatusType.SUCCESS.name( ), ResponseStatusType.SUCCESS.name( ), author, clientCode, secondaryMetadata );
AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, CANCEL_MERGE_IDENTITY_EVENT_CODE,
_internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( secondaryIdentity.getCustomerId( ) ),
SPECIFIC_ORIGIN );
final Map<String, String> primaryMetadata = new HashMap<>( );
primaryMetadata.put( Constants.METADATA_UNMERGED_CHILD_CUID, secondaryIdentity.getCustomerId( ) );
_identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.CONSOLIDATION_CANCELLED, primaryIdentity,
ResponseStatusType.SUCCESS.name( ), ResponseStatusType.SUCCESS.name( ), author, clientCode, primaryMetadata );
AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, CANCEL_CONSOLIDATE_IDENTITY_EVENT_CODE,
_internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( primaryIdentity.getCustomerId( ) ),
SPECIFIC_ORIGIN );
// refresh
secondaryIdentity = IdentityHome.findByCustomerId( request.getSecondaryCuid( ) );
return Pair.of( secondaryIdentity, attrStatusList );
}
/**
* Perform an identity research over a list of attributes (key and values) specified in the {@link IdentitySearchRequest}
*
* @param request
* the {@link IdentitySearchRequest} holding the parameters of the research
* @param author
* the author of the request
* @param serviceContract
* service contract of the client requesting the change
* @throws ResourceNotFoundException
* in case of {@link AttributeKey} management error
* @throws IdentityStoreException
* in case of unpredicted error
* @return list of matching identities
*/
public List<IdentityDto> search( final IdentitySearchRequest request, final RequestAuthor author, final ServiceContract serviceContract )
throws IdentityStoreException
{
final String clientCode = serviceContract.getClientCode( );
AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, SEARCH_IDENTITY_EVENT_CODE,
_internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( request.toString( ) ), SPECIFIC_ORIGIN );
final List<SearchAttribute> providedAttributes = request.getSearch( ).getAttributes( );
final QualifiedIdentitySearchResult result = _elasticSearchIdentityService.getQualifiedIdentities( providedAttributes, request.getMax( ),
request.isConnected( ), Collections.emptyList( ) );
if ( CollectionUtils.isNotEmpty( result.getQualifiedIdentities( ) ) )
{
final List<IdentityDto> filteredIdentities = this.getEnrichedIdentities( request.getSearch( ).getAttributes( ), serviceContract,
result.getQualifiedIdentities( ) );
if ( CollectionUtils.isNotEmpty( filteredIdentities ) )
{
for ( final IdentityDto identity : filteredIdentities )
{
AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, SEARCH_IDENTITY_EVENT_CODE,
_internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( identity.getCustomerId( ) ),
SPECIFIC_ORIGIN );
if ( author.getType( ).equals( AuthorType.agent ) )
{
/* Indexation et historique */
_identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.READ,
DtoConverter.convertDtoToIdentity( identity ), ResponseStatusType.OK.name( ), "Operation completed successfully", author,
clientCode, new HashMap<>( ) );
}
}
return filteredIdentities;
}
}
// return the empty list
return result.getQualifiedIdentities( );
}
/**
* Perform an identity research by customer or connection ID.
*
* @param customerId
* @param connectionId
* @param serviceContract
* @param author
* the author of the request
* @throws ResourceNotFoundException
*/
public IdentityDto search( final String customerId, final String connectionId, final ServiceContract serviceContract, final RequestAuthor author )
throws IdentityStoreException
{
final String clientCode = serviceContract.getClientCode( );
AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, GET_IDENTITY_EVENT_CODE, _internalUserService.getApiUser( clientCode ),
SecurityUtil.logForgingProtect( StringUtils.isNotBlank( customerId ) ? customerId : connectionId ), SPECIFIC_ORIGIN );
final IdentityDto identityDto = StringUtils.isNotBlank( customerId ) ? _identityDtoCache.getByCustomerId( customerId, serviceContract )
: _identityDtoCache.getByConnectionId( connectionId, serviceContract );
if ( identityDto == null )
{
// #345 : If the identity doesn't exist, make an extra search in the history (only for CUID search).
// If there is a record, it means the identity has been deleted => send back a specific message
if ( StringUtils.isNotBlank( customerId ) && !IdentityHome.findHistoryByCustomerId( customerId ).isEmpty( ) )
{
throw new ResourceNotFoundException( "The requested identity has been deleted.", Constants.PROPERTY_REST_ERROR_IDENTITY_DELETED );
}
else
{
throw new ResourceNotFoundException( "The requested identity could not be found.", Constants.PROPERTY_REST_ERROR_NO_IDENTITY_FOUND );
}
}
else
{
if ( author != null )
{
AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_READ, SEARCH_IDENTITY_EVENT_CODE,
_internalUserService.getApiUser( author, clientCode ), SecurityUtil.logForgingProtect( identityDto.getCustomerId( ) ),
SPECIFIC_ORIGIN );
}
if ( author != null && author.getType( ).equals( AuthorType.agent ) )
{
/* Indexation et historique */
_identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.READ, DtoConverter.convertDtoToIdentity( identityDto ),
ResponseStatusType.OK.name( ), "Operation completed successfully", author, clientCode, new HashMap<>( ) );
}
return identityDto;
}
}
/**
* Performs a search of a list of {@link IdentityDto}, providing a list of customer ids
*
* @param customerIds
* the customer ids to search for
* @return a list of {@link IdentityDto}
*/
public List<IdentityDto> search( final List<String> customerIds, final List<String> attributes )
{
try
{
final QualifiedIdentitySearchResult result = _elasticSearchIdentityService.getQualifiedIdentities( customerIds, attributes );
if ( result != null && CollectionUtils.isNotEmpty( result.getQualifiedIdentities( ) ) )
{
result.getQualifiedIdentities( ).forEach( identityDto -> {
IdentityQualityService.instance( ).computeQuality( identityDto );
identityDto.getQuality( ).setScoring( 1D );
identityDto.getQuality( ).setCoverage( 1 );
} );
return result.getQualifiedIdentities( );
}
}
catch( final IdentityStoreException e )
{
// ignore this identity
}
return new ArrayList<>( );
}
/**
* Performs a search of an {@link IdentityDto}, providing its customer id
*
* @param customerId
* the customer id to search for
* @return an {@link IdentityDto}
*/
public IdentityDto search( final String customerId )
{
try
{
final QualifiedIdentitySearchResult result = _elasticSearchIdentityService.getQualifiedIdentities( customerId, Collections.emptyList( ) );
if ( result != null && CollectionUtils.isNotEmpty( result.getQualifiedIdentities( ) ) )
{
final IdentityDto identityDto = result.getQualifiedIdentities( ).get( 0 );
IdentityQualityService.instance( ).computeQuality( identityDto );
identityDto.getQuality( ).setScoring( 1D );
identityDto.getQuality( ).setCoverage( 1 );
return identityDto;
}
}
catch( final IdentityStoreException e )
{
// ignore this identity
}
return null;
}
/**
* Filter a list of search results over {@link ServiceContract} defined for the given clientCode. Also complete identities with additional information
* (quality, duplicates, ...).
*
* @param searchAttributes
* la requête de recherche si existante
* @param serviceContract
* le contrat de service du demandeur
* @param identities
* la liste de résultats à traiter
* @return the list of filtered and completed {@link IdentityDto}
*/
private List<IdentityDto> getEnrichedIdentities( final List<SearchAttribute> searchAttributes, final ServiceContract serviceContract,
final List<IdentityDto> identities )
{
final Comparator<QualityDefinition> qualityComparator = Comparator.comparing( QualityDefinition::getScoring )
.thenComparingDouble( QualityDefinition::getQuality ).reversed( );
final Comparator<IdentityDto> identityComparator = Comparator.comparing( IdentityDto::getQuality, qualityComparator );
return identities.stream( ).filter( IdentityDto::isNotMerged )
.peek( identity -> IdentityQualityService.instance( ).enrich( searchAttributes, identity, serviceContract, null ) ).sorted( identityComparator )
.collect( Collectors.toList( ) );
}
/**
* Process full identity update with attributes of the requestIdentity
*
* @param identity
* @param requestIdentity
* @param clientCode
* @param metadata
* @param identityUpdateRequested
* @return the list of attribute status
* @throws IdentityStoreException
*/
private List<AttributeStatus> updateIdentity( final Identity identity, final IdentityDto requestIdentity, final String clientCode, final Map<String, String> metadata, boolean identityUpdateRequested)
throws IdentityStoreException
{
return updateIdentity( identity, requestIdentity, clientCode, metadata, identityUpdateRequested, false);
}
/**
* Process full identity update with attributes of the requestIdentity
*
* @param identity
* @param requestIdentity
* @param clientCode
* @param metadata
* @param identityUpdateRequested
* @return the list of attribute status
* @throws IdentityStoreException
*/
private List<AttributeStatus> updateIdentity( final Identity identity, final IdentityDto requestIdentity, final String clientCode,
final Map<String, String> metadata, boolean identityUpdateRequested, boolean keepOriginalCertification)
throws IdentityStoreException
{
final List<AttributeStatus> attrStatusList = new ArrayList<>( );
/* Récupération des attributs déja existants ou non */
final Map<Boolean, List<AttributeDto>> sortedAttributes = requestIdentity.getAttributes( ).stream( )
.collect( Collectors.partitioningBy( a -> identity.getAttributes( ).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<>( );
/* Create new attributes */
for ( final AttributeDto attributeToWrite : newWritableAttributes )
{
final AttributeStatus attributeStatus = _identityAttributeService.createAttribute( attributeToWrite,
identity, ( keepOriginalCertification?attributeToWrite.getLastUpdateClientCode ( ):clientCode ) );
attrStatusList.add( attributeStatus );
}
/* Update existing attributes */
for ( final AttributeDto attributeToUpdate : existingWritableAttributes )
{
final AttributeStatus attributeStatus = _identityAttributeService.updateAttribute( attributeToUpdate,
identity, ( keepOriginalCertification?attributeToUpdate.getLastUpdateClientCode ( ):clientCode ) );
attrStatusList.add( attributeStatus );
}
boolean monParisUpdated = false;
if ( requestIdentity.getMonParisActive( ) != null && requestIdentity.getMonParisActive( ) != identity.isMonParisActive( ) )
{
metadata.put(Constants.METADATA_NEW_MON_PARIS_ACTIF, String.valueOf( requestIdentity.getMonParisActive( ) ) );
metadata.put(Constants.METADATA_OLD_MON_PARIS_ACTIF, String.valueOf( identity.isMonParisActive( ) ) );
monParisUpdated = true;
identity.setMonParisActive( requestIdentity.isMonParisActive( ) );
}
if ( identityUpdateRequested || monParisUpdated || !Collections.disjoint( AttributeChangeStatus.getSuccessStatuses( ),
attrStatusList.stream( ).map( AttributeStatus::getStatus ).collect( Collectors.toList( ) ) ) )
{
// If there was an update on the monParis flag or in the attributes, we update the identity
IdentityHome.update( identity );
}
return attrStatusList;
}
/**
* Gets a list of qualified identities on which to search potential duplicates.<br/>
* Returned identities must have all attributes checked by the provided rule, and must also not be already merged nor be tagged as suspicious.<br/>
* The list is sorted by quality (higher quality identities first).
*
* @param rule
* the rule used to get matching identities
* @param batchSize the size of the batches
* @param includeSuspicions filter CUIDs, to include or not, the CUIDs that are identified as suspicions
* @return the list of identities
*/
public Batch<String> getCUIDsBatchForPotentialDuplicate(final DuplicateRule rule, final int batchSize, final boolean includeSuspicions )
{
final List<Integer> attributes = rule.getCheckedAttributes( ).stream( ).map( AttributeKey::getId ).collect( Collectors.toList( ) );
final List<String> customerIdsList = IdentityHome.findByAttributeExisting( attributes, rule.getNbFilledAttributes( ), true, !includeSuspicions, rule.getPriority() );
if ( customerIdsList.isEmpty( ) )
{
return Batch.ofSize( Collections.emptyList( ), 0 );
}
return Batch.ofSize( customerIdsList, batchSize );
}
/**
* request a deletion of identity .
*
* @param customerId
* the customer ID
* @param clientCode
* the client code
* @param author
* the author of the request
*/
public void deleteRequest( final String customerId, final String clientCode, final RequestAuthor author ) throws IdentityStoreException {
final Identity identity = IdentityHome.findByCustomerId(customerId);
TransactionManager.beginTransaction(null);
try {
// expire identity (the deletion is managed by the dedicated Daemon)
IdentityHome.softRemove(customerId);
TransactionManager.commitTransaction(null);
/* Notify listeners for indexation, history, ... */
_identityStoreNotifyListenerService.notifyListenersIdentityChange(IdentityChangeType.DELETE_REQUEST, identity, ResponseStatusType.SUCCESS.name(),
ResponseStatusType.SUCCESS.name(), author, clientCode, new HashMap<>());
} catch (final Exception e) {
TransactionManager.rollBack(null);
if (e instanceof IdentityStoreException) {
throw e;
}
throw new IdentityStoreException(e.getMessage(), e, Constants.PROPERTY_REST_ERROR_DURING_TREATMENT);
}
}
/**
* Dé-certification d'une identité.
*
* @param strCustomerId
* customer ID
* @return the response
* @see IdentityAttributeService#uncertifyAttribute
*/
public Pair<Identity, List<AttributeStatus>> uncertifyIdentity( final String strCustomerId, final List<AttributeKey> requestedAttributesKeys, final String strClientCode, final RequestAuthor author )
throws IdentityStoreException
{
final Identity identity = IdentityHome.findByCustomerId( strCustomerId );
final Collection<IdentityAttribute> attributesToDecertify;
if (!requestedAttributesKeys.isEmpty()) {
final List<String> requestedAttributeKeysStr = requestedAttributesKeys.stream().map(AttributeKey::getKeyName).collect(Collectors.toList());
// #27794 - if pivot requested to be decertified, and one of them is leveled >= PIVOT_UNCERTIF_LEVEL_THRESHOLD
// -> decertify all pivots
final int pivotUncertifyLevelThreshold = AppPropertiesService.getPropertyInt(PIVOT_UNCERTIF_LEVEL_THRESHOLD, 400);
final List<IdentityAttribute> pivotAttributes =
identity.getAttributes().values().stream().filter(a -> a.getAttributeKey().getPivot()).collect(Collectors.toList());
if (requestedAttributesKeys.stream().anyMatch(AttributeKey::getPivot) && pivotAttributes.stream().anyMatch(a -> AttributeCertificationDefinitionService.instance().getLevelAsInteger( a.getCertificate( ).getCertifierCode( ), a.getAttributeKey().getKeyName() ) >= pivotUncertifyLevelThreshold)) {
attributesToDecertify = identity.getAttributes().values().stream().filter(a -> a.getAttributeKey().getPivot() || requestedAttributeKeysStr.contains(a.getAttributeKey().getKeyName())).collect(Collectors.toList());
} else {
attributesToDecertify = identity.getAttributes().values().stream().filter(a -> requestedAttributeKeysStr.contains(a.getAttributeKey().getKeyName())).collect(Collectors.toList());
}
} else {
attributesToDecertify = identity.getAttributes().values();
}
if (attributesToDecertify.isEmpty()) {
throw new IdentityStoreException( "No attributes to decertify", Constants.PROPERTY_REST_ERROR_DURING_TREATMENT);
}
TransactionManager.beginTransaction( null );
try
{
final List<AttributeStatus> attrStatusList = new ArrayList<>( );
for ( final IdentityAttribute attribute : attributesToDecertify)
{
final AttributeStatus status = _identityAttributeService.uncertifyAttribute( attribute );
attrStatusList.add( status );
}
// update identity to set lastupdate_date
IdentityHome.update( identity );
TransactionManager.commitTransaction( null );
/* Historique des modifications */
for ( AttributeStatus attributeStatus : attrStatusList )
{
_identityStoreNotifyListenerService.notifyListenersAttributeChange( AttributeChangeType.UPDATE, identity, attributeStatus, author,
strClientCode );
}
/* Indexation et historique */
_identityStoreNotifyListenerService.notifyListenersIdentityChange( IdentityChangeType.UPDATE, identity, ResponseStatusType.SUCCESS.name( ),
ResponseStatusType.SUCCESS.name( ), author, strClientCode, new HashMap<>( ) );
AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, DECERTIFY_IDENTITY_EVENT_CODE,
_internalUserService.getApiUser( author, strClientCode ), SecurityUtil.logForgingProtect( strCustomerId ), SPECIFIC_ORIGIN );
return Pair.of( identity, attrStatusList );
}
catch( final Exception e )
{
TransactionManager.rollBack( null );
if ( e instanceof IdentityStoreException )
{
throw e;
}
throw new IdentityStoreException( e.getMessage( ), e, Constants.PROPERTY_REST_ERROR_DURING_TREATMENT );
}
}
/**
* Delete the identity and all his children, including potential merged identities, EXCEPT identity history.<br/>
* The purge consist of deleting, for the identity and all of its potential merged identities :
* <ul>
* <li>the {@link Identity} object</li>
* <li>the {@link IdentityAttribute} objetcs</li>
* <li>the IdentityAttributeHistory objects</li>
* <li>the {@link SuspiciousIdentity} objects</li>
* <li>the {@link ExcludedIdentities} objects</li>
* </ul>
* The identity's history is kept.
*
* @param customerId
* the customerId of the identity to delete
*/
public void delete( final String customerId )
{
Identity identity = IdentityHome.findByCustomerId( customerId );
if ( identity != null && identity.getId( ) != -1 )
{
final List<Identity> mergedIdentities = IdentityHome.findMergedIdentities( identity.getId( ) );
TransactionManager.beginTransaction( null );
try
{
// Delete eventual merged identities first
for ( final Identity mergedIdentity : mergedIdentities )
{
SuspiciousIdentityHome.remove( mergedIdentity.getCustomerId( ) );
SuspiciousIdentityHome.removeExcludedIdentities( mergedIdentity.getCustomerId( ) );
IdentityHome.deleteAttributeHistory( mergedIdentity.getId( ) );
IdentityHome.hardRemove( mergedIdentity.getId( ) );
}
// Delete the actual identity
SuspiciousIdentityHome.remove( customerId );
SuspiciousIdentityHome.removeExcludedIdentities( customerId );
IdentityHome.deleteAttributeHistory( identity.getId( ) );
IdentityHome.hardRemove( identity.getId( ) );
/* Notify listeners for indexation, history, ... */
_identityStoreNotifyListenerService.notifyListenersIdentityChange(IdentityChangeType.DELETE_REQUEST, identity, ResponseStatusType.SUCCESS.name(),
ResponseStatusType.SUCCESS.name(), new RequestAuthor ("DAEMON", AuthorType.application.name ( ) ), "DAEMON", new HashMap<>());
AccessLogService.getInstance().info(AccessLoggerConstants.EVENT_TYPE_DELETE, DELETE_IDENTITY_EVENT_CODE,
null, SecurityUtil.logForgingProtect(customerId), SPECIFIC_ORIGIN);
TransactionManager.commitTransaction( null );
}
catch( final Exception e )
{
TransactionManager.rollBack( null );
throw e;
}
}
}
/**
* Search for updated identities according to the provided request
*
* @param request
* the request
* @return the updated identities, along with the pagination (or null if none requested)
* @throws IdentityStoreException
* in case of error
*/
public Pair<List<UpdatedIdentityDto>, Page> searchUpdatedIdentities( final UpdatedIdentitySearchRequest request ) throws IdentityStoreException
{
final int maxFromProperty = AppPropertiesService.getPropertyInt( PROPERTY_MAX_RESULT_UPDATED_IDENTITY_SEARCH, 500 );
final int maxResult = request.getMax( ) == null ? maxFromProperty : Math.min( request.getMax( ), maxFromProperty );
final List<UpdatedIdentityDto> updatedIdentities;
if ( request.getPage( ) != null && request.getSize( ) != null )
{
// Si pagination
// première requête qui ne ramène que les ID (avec un LIMIT ${maxResult} ), triés par date de derniere modification
final List<Integer> allUpdatedIdentityIds = IdentityHome.findUpdatedIdentityIds( request.getDays( ), request.getIdentityChangeTypes( ),
request.getUpdatedAttributes( ), maxResult );
final int totalRecords = allUpdatedIdentityIds.size( );
if ( totalRecords == 0 )
{
throw new ResourceNotFoundException( "No updated identity found with the provided criterias.",
Constants.PROPERTY_REST_ERROR_NO_UPDATED_IDENTITY_FOUND );
}
final int totalPages = (int) Math.ceil( (double) totalRecords / request.getSize( ) );
if ( request.getPage( ) > totalPages )
{
throw new RequestFormatException( "Pagination index should not exceed total number of pages.", Constants.PROPERTY_REST_PAGINATION_END_ERROR );
}
final Page pagination = new Page( );
pagination.setTotalPages( totalPages );
pagination.setTotalRecords( totalRecords );
pagination.setCurrentPage( request.getPage( ) );
pagination.setNextPage( request.getPage( ) == totalPages ? null : request.getPage( ) + 1 );
pagination.setPreviousPage( request.getPage( ) > 1 ? request.getPage( ) - 1 : null );
// deuxième requête qui prend les IDs correspondant à la page demandée (sublist), et qui va chercher les data
final int start = ( request.getPage( ) - 1 ) * request.getSize( );
final int end = Math.min( start + request.getSize( ), totalRecords );
updatedIdentities = IdentityHome.getUpdatedIdentitiesFromIds( allUpdatedIdentityIds.subList( start, end ) );
return Pair.of( updatedIdentities, pagination );
}
else
{
// Pas de pagination demandée, une seule requête qui ramène directement les datas (avec un LIMIT ${maxResult} )
updatedIdentities = IdentityHome.findUpdatedIdentities( request.getDays( ), request.getIdentityChangeTypes( ), request.getUpdatedAttributes( ),
maxResult );
if ( updatedIdentities.isEmpty( ) )
{
throw new ResourceNotFoundException( "No updated identity found with the provided criterias.",
Constants.PROPERTY_REST_ERROR_NO_UPDATED_IDENTITY_FOUND );
}
return Pair.of( updatedIdentities, null );
}
}
}