View Javadoc
1   /*
2    * Copyright (c) 2002-2024, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.plugins.identitystore.service;
35  
36  import static fr.paris.lutece.plugins.identitystore.service.identity.IdentityService.UPDATE_IDENTITY_EVENT_CODE;
37  
38  import java.sql.Date;
39  import java.sql.Timestamp;
40  import java.time.Instant;
41  import java.time.LocalDateTime;
42  import java.time.ZoneId;
43  import java.time.ZonedDateTime;
44  import java.util.ArrayList;
45  import java.util.Collections;
46  import java.util.HashMap;
47  import java.util.List;
48  import java.util.Map;
49  
50  import org.apache.commons.lang3.StringUtils;
51  
52  import fr.paris.lutece.plugins.grubusiness.business.demand.DemandType;
53  import fr.paris.lutece.plugins.grubusiness.business.web.rs.DemandDisplay;
54  import fr.paris.lutece.plugins.grubusiness.business.web.rs.DemandResult;
55  import fr.paris.lutece.plugins.grubusiness.business.web.rs.EnumGenericStatus;
56  import fr.paris.lutece.plugins.identitystore.business.contract.ServiceContract;
57  import fr.paris.lutece.plugins.identitystore.business.identity.Identity;
58  import fr.paris.lutece.plugins.identitystore.business.identity.IdentityHome;
59  import fr.paris.lutece.plugins.identitystore.service.contract.ServiceContractService;
60  import fr.paris.lutece.plugins.identitystore.service.identity.IdentityService;
61  import fr.paris.lutece.plugins.identitystore.service.listeners.IdentityStoreNotifyListenerService;
62  import fr.paris.lutece.plugins.identitystore.service.user.InternalUserService;
63  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.AuthorType;
64  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.RequestAuthor;
65  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.common.ResponseStatusType;
66  import fr.paris.lutece.plugins.identitystore.v3.web.rs.dto.history.IdentityChangeType;
67  import fr.paris.lutece.plugins.identitystore.web.exception.ClientAuthorizationException;
68  import fr.paris.lutece.plugins.notificationstore.v1.web.service.NotificationStoreService;
69  import fr.paris.lutece.portal.service.security.AccessLogService;
70  import fr.paris.lutece.portal.service.security.AccessLoggerConstants;
71  import fr.paris.lutece.portal.service.spring.SpringContextService;
72  import fr.paris.lutece.portal.service.util.AppLogService;
73  import fr.paris.lutece.portal.service.util.AppPropertiesService;
74  
75  public final class PurgeIdentityService
76  {
77      public static final String KEY_INFOS_ARE_MISSING = "isInfosAreMissing";
78      public static final String KEY_AT_LEAST_ONE_CS_FOUND = "isAtLeastOneServiceContractFound";
79  
80      private static PurgeIdentityService _instance;
81  
82      private final NotificationStoreService _notificationStoreService = SpringContextService.getBean( "notificationStore.notificationStoreService" );
83      private final IdentityStoreNotifyListenerService _identityStoreNotifyListenerService = IdentityStoreNotifyListenerService.instance( );
84  
85      public static PurgeIdentityService getInstance( )
86      {
87  	if ( _instance == null )
88  	{
89  	    _instance = new PurgeIdentityService( );
90  	}
91  	return _instance;
92      }
93  
94      /**
95       * purge identities
96       *
97       * @return log {@link StringBuilder}
98       */
99      public String purge( final RequestAuthor daemonAuthor, final String daemonClientCode, final List<String> excludedAppCodes, final int batchLimit )
100     {
101 	final StringBuilder msg = new StringBuilder( );
102 	final boolean isDryRun = AppPropertiesService.getPropertyBoolean ( "daemon.purgeIdentityDaemon.dryRun", false ); 
103 	final int nMaxCguRetentionPeridodInMonths = AppPropertiesService.getPropertyInt( "daemon.purgeIdentityDaemon.maxCguRetentionPeridodInMonths", 120 ); 
104 
105 	// search identities with a passed peremption date, not merged to a primary identity, and not associated to a MonParis account
106 	final List<Identity> expiredIdentities = IdentityHome.findExpiredNotMergedAndNotConnectedIdentities( 
107 		batchLimit, AppPropertiesService.getPropertyBoolean ( "daemon.purgeIdentityDaemon.withGuidOnly", true) );
108 	final Timestamp now = Timestamp.from( Instant.now( ) );
109 	final Timestamp olderCguRetentionDate = Timestamp.valueOf ( now.toLocalDateTime ( ).minusMonths ( nMaxCguRetentionPeridodInMonths ) );
110 
111 	msg.append( expiredIdentities.size( ) ).append( " expired identities found" ).append( "\n" );
112 
113 	for ( final Identity expiredIdentity : expiredIdentities )
114 	{
115 	    try
116 	    {
117 		final List<Identity> mergedIdentities = IdentityHome.findMergedIdentities( expiredIdentity.getId( ) );
118 
119 		// Get Demands notifications associated to each identity or its merged ones
120 		DemandResult demandResult = _notificationStoreService.getListDemand( expiredIdentity.getCustomerId( ), null, null, null, null );
121 		final List<DemandDisplay> demandDisplayList = demandResult.getListDemandDisplay() == null ? new ArrayList<>() : new ArrayList<>(demandResult.getListDemandDisplay());
122 		for ( final Identity mergedIdentity : mergedIdentities )
123 		{
124 		    final DemandResult mergedDemands = _notificationStoreService.getListDemand( mergedIdentity.getCustomerId( ), null, null, null, null );
125 		    if ( mergedDemands.getListDemandDisplay( ) != null )
126 		    {
127 			demandDisplayList.addAll( mergedDemands.getListDemandDisplay( ) );
128 		    }
129 		}
130 
131 		Map<String,Boolean> indicators = new HashMap<>( );
132 		indicators.put( KEY_AT_LEAST_ONE_CS_FOUND, false);
133 		indicators.put( KEY_INFOS_ARE_MISSING, false);
134 
135 		// Calculate the new expiration date (date of demand last update + CGUs term)
136 		Timestamp demandExpirationDateMAX = expiredIdentity.getExpirationDate( );
137 		for ( final DemandDisplay demand : demandDisplayList )
138 		{
139 		    final Timestamp expirationDateFromDemand = checkExpirationDateByDemand( demand, 
140 			    indicators, excludedAppCodes, msg, _notificationStoreService );
141 
142 		    // keep the max expiration date
143 		    if ( expirationDateFromDemand != null && demandExpirationDateMAX.before( expirationDateFromDemand ) )
144 		    {
145 			demandExpirationDateMAX = expirationDateFromDemand;
146 		    }
147 		}
148 
149 
150 		// check if expiredIdentity should be preserved or can be deleted
151 		if ( demandExpirationDateMAX.after( now ) )
152 		{
153 		    // expiration date calculated from most recent demand is later than today
154 		    // => update the expiration date of expiredIdentity : it will be deleted later
155 		    expiredIdentity.setExpirationDate( demandExpirationDateMAX );
156 		    IdentityHome.update( expiredIdentity );
157 
158 		    // re-index and add history
159 		    IdentityStoreNotifyListenerService.instance( ).notifyListenersIdentityChange( IdentityChangeType.UPDATE, expiredIdentity,
160 			    "EXPIRATION_POSTPONED", StringUtils.EMPTY, daemonAuthor, daemonClientCode, Collections.emptyMap( ) );
161 		    AccessLogService.getInstance( ).info( AccessLoggerConstants.EVENT_TYPE_MODIFY, UPDATE_IDENTITY_EVENT_CODE,
162 			    InternalUserService.getInstance( ).getApiUser( daemonAuthor, daemonClientCode ), null, "PURGE_DAEMON" );
163 
164 		    msg.append( "Identity expiration date updated : [" ).append( expiredIdentity.getCustomerId( ) ).append( "]" ).append( "\n" );
165 		}
166 		else if ( indicators.get( KEY_INFOS_ARE_MISSING ) && demandExpirationDateMAX.after( olderCguRetentionDate ) )
167 		{
168 		    // if infos are missing (but the maximum CGU retention period has not been reached)
169 		    if ( !indicators.get(  KEY_AT_LEAST_ONE_CS_FOUND ) )
170 		    {
171 			msg.append ( "No service contact found for identity [").append( expiredIdentity.getCustomerId( ) ).append ( "]\n");
172 		    }
173 
174 		    // not enough infos to delete identity
175 		    msg.append( "Unsufficient infos to delete [" ).append( expiredIdentity.getCustomerId( ) ).append( "]" ).append( "\n" );
176 
177 		    // Notify listeners 
178 		    _identityStoreNotifyListenerService.notifyListenersIdentityChange(IdentityChangeType.DELETION_ATTEMPT_FAILED, expiredIdentity, 
179 			    ResponseStatusType.UNAUTHORIZED.name(),ResponseStatusType.UNAUTHORIZED.name(), 
180 			    new RequestAuthor ("DAEMON", AuthorType.application.name ( ) ), "DAEMON", null);
181 
182 		}
183 		else if ( isDryRun )
184 		{
185 		    // Dry run : test mode only (no deletion)
186 		    msg.append( "(Dry run) should Detete Identity [" ).append( expiredIdentity.getCustomerId( ) ).append( "]" ).append( "\n" );
187 		}
188 		else 
189 		{
190 		    // if the updated peremption date still passed, delete the identity (and children as merged identities,
191 		    // suspicious, attributes and attributes history, etc ...) EXCEPT the identity history
192 		    IdentityService.instance( ).delete( expiredIdentity.getCustomerId( ) );
193 		    msg.append( "Detete Identity [" ).append( expiredIdentity.getCustomerId( ) ).append( "]" ).append( "\n" );
194 
195 		    // delete notifications
196 		    _notificationStoreService.deleteNotificationByCuid( expiredIdentity.getCustomerId( ) );
197 		    msg.append( "Notifications deleted for main identity [" ).append( expiredIdentity.getCustomerId( ) ).append( "]" ).append( "\n" );
198 		    for ( final Identity mergedIdentity : mergedIdentities )
199 		    {
200 			_notificationStoreService.deleteNotificationByCuid( mergedIdentity.getCustomerId( ) );
201 			msg.append( "Notifications deleted for merged identity [" ).append( mergedIdentity.getCustomerId( ) ).append( "]" ).append( "\n" );
202 		    }
203 		}
204 	    }
205 	    catch( final Exception e )
206 	    {
207 		msg.append( "Daemon execution error for identity : " ).append( expiredIdentity.getCustomerId( ) ).append( " :: " ).append( e.getMessage( ) )
208 		.append( "\n" );
209 		return msg.toString( );
210 	    }
211 	}
212 
213 	// return message for daemons
214 	return msg.toString( );
215     }
216 
217     /**
218      * get app code
219      *
220      * @param strTypeId
221      *            the type id
222      * @return the app code
223      */
224     public static String getAppCodeFromDemandTypeId( final String strTypeId, NotificationStoreService notificationStoreService )
225     {
226 	DemandType demandType = notificationStoreService.getDemandType( strTypeId );
227 
228 	if ( demandType != null )
229 	{
230 	    return demandType.getAppCode( );
231 	}
232 
233 	return null;
234     }
235 
236     /**
237      * check Expiration Date By Demand
238      * 
239      * @param demand
240      * @param isAtLeastOneServiceContractFound
241      * @param isInfosAreMissing
242      * @param excludedAppCodes
243      * @param msg
244      * @return 
245      * @throws ClientAuthorizationException 
246      */
247     public static Timestamp checkExpirationDateByDemand( DemandDisplay demand, 
248 	    Map<String,Boolean> indicators, List<String> excludedAppCodes, StringBuilder msg,
249 	    NotificationStoreService notificationStoreService) throws ClientAuthorizationException
250     {
251 	// ignore canceled demands
252 	if (demand.getDemand().getStatusId() == EnumGenericStatus.CANCELED.getStatusId()) {
253 	    return null;
254 	}
255 
256 	final String appCode = getAppCodeFromDemandTypeId( demand.getDemand( ).getTypeId( ), notificationStoreService );
257 	if ( appCode == null )
258 	{
259 	    msg.append( "Unknown AppCode for demand_type_id : ")
260 	    .append ( (demand.getDemand( ).getTypeId( )!=null?demand.getDemand( ).getTypeId( ):"") )
261 	    .append ( "\n" );
262 	    AppLogService.info( "CheckExpirationDate / Unknown AppCode for demand_type_id : "  
263 		    + (demand.getDemand( ).getTypeId( )!=null?demand.getDemand( ).getTypeId( ):"") );
264 
265 	    indicators.put( KEY_INFOS_ARE_MISSING, true);
266 	}
267 
268 	if ( appCode!=null && !excludedAppCodes.contains( appCode.toUpperCase( ) ) )
269 	{
270 	    final List<String> clientCodeList = ServiceContractService.instance( ).getClientCodesFromAppCode( appCode.toUpperCase( ) );
271 
272 	    int nbMonthsCGUsMAX = 0;
273 	    for ( final String clientCode : clientCodeList )
274 	    {
275 		// if there is more than one client code for the app_code, keep the max value of cgus
276 		Timestamp tscreate = new Timestamp( demand.getDemand( ).getCreationDate( ) );
277 		Date demandCreationDate = Date.valueOf( tscreate.toLocalDateTime( ).toLocalDate( ) );
278 
279 		ServiceContract sc = ServiceContractService.instance( ).getActiveServiceContractAtSpecificDate( clientCode, demandCreationDate );
280 		if ( sc == null )
281 		{
282 		    sc = ServiceContractService.instance( ).getActiveServiceContract( clientCode );
283 		}
284 
285 		if ( sc != null )
286 		{
287 		    indicators.put( KEY_AT_LEAST_ONE_CS_FOUND, true); 
288 		    
289 		    if ( sc.getDataRetentionPeriodInMonths( ) > nbMonthsCGUsMAX )
290 		    {
291 			nbMonthsCGUsMAX = sc.getDataRetentionPeriodInMonths( );
292 		    }
293 		}
294 		else
295 		{
296 		    indicators.put( KEY_INFOS_ARE_MISSING, true);
297 		}
298 	    }
299 
300 	    final ZonedDateTime demandDate = ZonedDateTime.ofInstant( Instant.ofEpochMilli( demand.getDemand( ).getModifyDate( ) ),
301 		    ZoneId.systemDefault( ) );
302 	    final Timestamp expirationDateFromDemand = Timestamp.from( demandDate.plusMonths( nbMonthsCGUsMAX ).toInstant( ) );
303 
304 	    return expirationDateFromDemand;
305 	}
306 
307 	return null;
308     }
309 }