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( "]" )
187 		    	.append( " - Notified demands found : " ).append(  demandDisplayList.size( ) ).append( "\n" );
188 		    
189 		    if ( demandExpirationDateMAX.before( olderCguRetentionDate ) )
190 		    {
191 			 msg.append( " ( demand Expiration Date MAX = " ).append( demandExpirationDateMAX )
192 			 	.append( ") < MAX Cgu Retention Date = ").append( olderCguRetentionDate  )
193 			 	.append( " )" );
194 		    }
195 		}
196 		else 
197 		{
198 		    // if the updated peremption date still passed, delete the identity (and children as merged identities,
199 		    // suspicious, attributes and attributes history, etc ...) EXCEPT the identity history
200 		    IdentityService.instance( ).delete( expiredIdentity.getCustomerId( ) );
201 		    msg.append( "Detete Identity [" ).append( expiredIdentity.getCustomerId( ) ).append( "]" )
202 		    	.append( " - Notified demands found : " ).append(  demandDisplayList.size( ) ).append( "\n" );
203 		    
204 		    if ( demandExpirationDateMAX.before( olderCguRetentionDate ) )
205 		    {
206 			 msg.append( " ( demand Expiration Date MAX = " ).append( demandExpirationDateMAX )
207 			 	.append( ") < MAX Cgu Retention Date = ").append( olderCguRetentionDate  )
208 			 	.append( " )" );
209 		    }
210 		    
211 		    if ( demandDisplayList.size( ) > 0 )
212 		    {
213 			// delete notifications
214 			_notificationStoreService.deleteNotificationByCuid( expiredIdentity.getCustomerId( ) );
215 			msg.append( "Notifications deleted for main identity [" ).append( expiredIdentity.getCustomerId( ) ).append( "]" ).append( "\n" );
216 			for ( final Identity mergedIdentity : mergedIdentities )
217 			{
218 			    _notificationStoreService.deleteNotificationByCuid( mergedIdentity.getCustomerId( ) );
219 			    msg.append( "Notifications deleted for merged identity [" ).append( mergedIdentity.getCustomerId( ) ).append( "]" ).append( "\n" );
220 			}
221 		    }
222 		}
223 	    }
224 	    catch( final Exception e )
225 	    {
226 		msg.append( "Daemon execution error for identity : " ).append( expiredIdentity.getCustomerId( ) ).append( " :: " ).append( e.getMessage( ) )
227 		.append( "\n" );
228 		return msg.toString( );
229 	    }
230 	}
231 
232 	// return message for daemons
233 	return msg.toString( );
234     }
235 
236     /**
237      * get app code
238      *
239      * @param strTypeId
240      *            the type id
241      * @return the app code
242      */
243     public static String getAppCodeFromDemandTypeId( final String strTypeId, NotificationStoreService notificationStoreService )
244     {
245 	DemandType demandType = notificationStoreService.getDemandType( strTypeId );
246 
247 	if ( demandType != null )
248 	{
249 	    return demandType.getAppCode( );
250 	}
251 
252 	return null;
253     }
254 
255     /**
256      * check Expiration Date By Demand
257      * 
258      * @param demand
259      * @param isAtLeastOneServiceContractFound
260      * @param isInfosAreMissing
261      * @param excludedAppCodes
262      * @param msg
263      * @return 
264      * @throws ClientAuthorizationException 
265      */
266     public static Timestamp checkExpirationDateByDemand( DemandDisplay demand, 
267 	    Map<String,Boolean> indicators, List<String> excludedAppCodes, StringBuilder msg,
268 	    NotificationStoreService notificationStoreService) throws ClientAuthorizationException
269     {
270 	// ignore canceled demands
271 	if (demand.getDemand().getStatusId() == EnumGenericStatus.CANCELED.getStatusId()) {
272 	    return null;
273 	}
274 
275 	final String appCode = getAppCodeFromDemandTypeId( demand.getDemand( ).getTypeId( ), notificationStoreService );
276 
277 	// unknown app_code
278 	if ( appCode == null )
279 	{
280 	    msg.append( "Unknown AppCode for demand_type_id : ")
281 	    .append ( (demand.getDemand( ).getTypeId( )!=null?demand.getDemand( ).getTypeId( ):"") )
282 	    .append ( "\n" );
283 	    AppLogService.info( "CheckExpirationDate / Unknown AppCode for demand_type_id : "  
284 		    + (demand.getDemand( ).getTypeId( )!=null?demand.getDemand( ).getTypeId( ):"") );
285 
286 	    indicators.put( KEY_INFOS_ARE_MISSING, true);
287 	    return null;
288 	}
289 
290 	// ignore excluded app_codes
291 	if ( excludedAppCodes.contains( appCode.toUpperCase( ) ) )
292 	{
293 	    // do not consider notifications from excluded app_codes
294 	    return null;
295 	}
296 
297 	final List<String> clientCodeList = ServiceContractService.instance( ).getClientCodesFromAppCode( appCode.toUpperCase( ) );
298 
299 	int nbMonthsCGUsMAX = 0;
300 	for ( final String clientCode : clientCodeList )
301 	{
302 	    // if there is more than one client code for the app_code, keep the max value of cgus
303 	    Timestamp tscreate = new Timestamp( demand.getDemand( ).getCreationDate( ) );
304 	    Date demandCreationDate = Date.valueOf( tscreate.toLocalDateTime( ).toLocalDate( ) );
305 
306 	    // search active service contract at notification date
307 	    ServiceContract sc = ServiceContractService.instance( ).getActiveServiceContractAtSpecificDate( clientCode, demandCreationDate );
308 	    if ( sc == null )
309 	    {
310 		try 
311 		{
312 		    // otherwise, consider the active service contract
313 		    sc = ServiceContractService.instance( ).getActiveServiceContract( clientCode );
314 		} 
315 		catch (ClientAuthorizationException e )
316 		{
317 		    // do nothing
318 		}
319 	    }
320 
321 	    if ( sc != null )
322 	    {
323 		// at least one service contract found
324 		indicators.put( KEY_AT_LEAST_ONE_CS_FOUND, true); 
325 
326 		if ( sc.getDataRetentionPeriodInMonths( ) > nbMonthsCGUsMAX )
327 		{
328 		    nbMonthsCGUsMAX = sc.getDataRetentionPeriodInMonths( );
329 		}
330 	    }
331 	    else
332 	    {
333 		// some informations are missing : this will be logged and stop deletion process
334 		indicators.put( KEY_INFOS_ARE_MISSING, true);
335 	    }
336 	}
337 
338 	final ZonedDateTime demandDate = ZonedDateTime.ofInstant( Instant.ofEpochMilli( demand.getDemand( ).getModifyDate( ) ),
339 		ZoneId.systemDefault( ) );
340 	final Timestamp expirationDateFromDemand = Timestamp.from( demandDate.plusMonths( nbMonthsCGUsMAX ).toInstant( ) );
341 
342 	return expirationDateFromDemand;
343     }
344 }