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