PwNotifyEngine.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. /*
  2. * Password Management Servlets (PWM)
  3. * http://www.pwm-project.org
  4. *
  5. * Copyright (c) 2006-2009 Novell, Inc.
  6. * Copyright (c) 2009-2020 The PWM Project
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. package password.pwm.svc.pwnotify;
  21. import com.novell.ldapchai.ChaiUser;
  22. import password.pwm.PwmApplication;
  23. import password.pwm.bean.EmailItemBean;
  24. import password.pwm.bean.SessionLabel;
  25. import password.pwm.bean.UserIdentity;
  26. import password.pwm.config.PwmSetting;
  27. import password.pwm.config.value.data.UserPermission;
  28. import password.pwm.error.PwmError;
  29. import password.pwm.error.PwmOperationalException;
  30. import password.pwm.error.PwmUnrecoverableException;
  31. import password.pwm.ldap.LdapOperationsHelper;
  32. import password.pwm.ldap.UserInfo;
  33. import password.pwm.ldap.UserInfoFactory;
  34. import password.pwm.ldap.permission.UserPermissionTester;
  35. import password.pwm.svc.stats.Statistic;
  36. import password.pwm.svc.stats.StatisticsManager;
  37. import password.pwm.util.PwmScheduler;
  38. import password.pwm.util.i18n.LocaleHelper;
  39. import password.pwm.util.java.ConditionalTaskExecutor;
  40. import password.pwm.util.java.JavaHelper;
  41. import password.pwm.util.java.TimeDuration;
  42. import password.pwm.util.logging.PwmLogger;
  43. import password.pwm.util.macro.MacroMachine;
  44. import java.io.IOException;
  45. import java.io.Writer;
  46. import java.time.Duration;
  47. import java.time.Instant;
  48. import java.time.temporal.ChronoUnit;
  49. import java.util.ArrayDeque;
  50. import java.util.List;
  51. import java.util.Locale;
  52. import java.util.Optional;
  53. import java.util.Queue;
  54. import java.util.concurrent.LinkedBlockingDeque;
  55. import java.util.concurrent.ThreadFactory;
  56. import java.util.concurrent.ThreadPoolExecutor;
  57. import java.util.concurrent.TimeUnit;
  58. import java.util.concurrent.atomic.AtomicInteger;
  59. import java.util.function.Supplier;
  60. public class PwNotifyEngine
  61. {
  62. private static final PwmLogger LOGGER = PwmLogger.forClass( PwNotifyEngine.class );
  63. private static final SessionLabel SESSION_LABEL = SessionLabel.PW_EXP_NOTICE_LABEL;
  64. private static final int MAX_LOG_SIZE = 1024 * 1024 * 1024;
  65. private final PwNotifySettings settings;
  66. private final PwmApplication pwmApplication;
  67. private final Writer debugWriter;
  68. private final StringBuffer internalLog = new StringBuffer( );
  69. private final List<UserPermission> permissionList;
  70. private final PwNotifyStorageService storageService;
  71. private final Supplier<Boolean> cancelFlag;
  72. private final ConditionalTaskExecutor debugOutputTask = new ConditionalTaskExecutor(
  73. this::periodicDebugOutput,
  74. new ConditionalTaskExecutor.TimeDurationPredicate( 1, TimeDuration.Unit.MINUTES )
  75. );
  76. private final AtomicInteger examinedCount = new AtomicInteger( 0 );
  77. private final AtomicInteger noticeCount = new AtomicInteger( 0 );
  78. private Instant startTime;
  79. private volatile boolean running;
  80. PwNotifyEngine(
  81. final PwmApplication pwmApplication,
  82. final PwNotifyStorageService storageService,
  83. final Supplier<Boolean> cancelFlag,
  84. final Writer debugWriter
  85. )
  86. {
  87. this.pwmApplication = pwmApplication;
  88. this.cancelFlag = cancelFlag;
  89. this.storageService = storageService;
  90. this.settings = PwNotifySettings.fromConfiguration( pwmApplication.getConfig() );
  91. this.debugWriter = debugWriter;
  92. this.permissionList = pwmApplication.getConfig().readSettingAsUserPermission( PwmSetting.PW_EXPY_NOTIFY_PERMISSION );
  93. }
  94. public boolean isRunning()
  95. {
  96. return running;
  97. }
  98. public String getDebugLog()
  99. {
  100. return internalLog.toString();
  101. }
  102. private boolean checkIfRunningOnMaster( )
  103. {
  104. if ( !pwmApplication.getPwmEnvironment().isInternalRuntimeInstance() )
  105. {
  106. if ( pwmApplication.getClusterService() != null && pwmApplication.getClusterService().isMaster() )
  107. {
  108. return true;
  109. }
  110. }
  111. return false;
  112. }
  113. boolean canRunOnThisServer()
  114. {
  115. return checkIfRunningOnMaster();
  116. }
  117. void executeJob( )
  118. throws PwmOperationalException, PwmUnrecoverableException
  119. {
  120. startTime = Instant.now();
  121. examinedCount.set( 0 );
  122. noticeCount.set( 0 );
  123. try
  124. {
  125. internalLog.delete( 0, internalLog.length() );
  126. running = true;
  127. if ( !canRunOnThisServer() || cancelFlag.get() )
  128. {
  129. return;
  130. }
  131. if ( JavaHelper.isEmpty( permissionList ) )
  132. {
  133. log( "no users are included in permission list setting "
  134. + PwmSetting.PW_EXPY_NOTIFY_PERMISSION.toMenuLocationDebug( null, null )
  135. + ", exiting."
  136. );
  137. return;
  138. }
  139. log( "starting job, beginning ldap search" );
  140. final Queue<UserIdentity> workQueue = new ArrayDeque<>( UserPermissionTester.discoverMatchingUsers(
  141. pwmApplication,
  142. permissionList, SESSION_LABEL, settings.getMaxLdapSearchSize(),
  143. settings.getSearchTimeout()
  144. ) );
  145. log( "ldap search complete, examining users..." );
  146. final ThreadPoolExecutor threadPoolExecutor = createExecutor( pwmApplication );
  147. while ( workQueue.peek() != null )
  148. {
  149. if ( !checkIfRunningOnMaster() || cancelFlag.get() )
  150. {
  151. final String msg = "job interrupted, server is no longer the cluster master.";
  152. log( msg );
  153. throw PwmUnrecoverableException.newException( PwmError.ERROR_SERVICE_NOT_AVAILABLE, msg );
  154. }
  155. threadPoolExecutor.submit( new ProcessJob( workQueue.poll() ) );
  156. }
  157. JavaHelper.closeAndWaitExecutor( threadPoolExecutor, TimeDuration.DAY );
  158. log( "job complete, " + examinedCount + " users evaluated in " + TimeDuration.fromCurrent( startTime ).asCompactString()
  159. + ", sent " + noticeCount + " notices."
  160. );
  161. }
  162. catch ( final PwmUnrecoverableException | PwmOperationalException e )
  163. {
  164. log( "error while executing job: " + e.getMessage() );
  165. throw e;
  166. }
  167. finally
  168. {
  169. running = false;
  170. }
  171. }
  172. private void periodicDebugOutput()
  173. {
  174. final String msg = "job in progress, " + examinedCount + " users evaluated in "
  175. + TimeDuration.fromCurrent( startTime ).asCompactString()
  176. + ", sent " + noticeCount + " notices.";
  177. log( msg );
  178. }
  179. private class ProcessJob implements Runnable
  180. {
  181. final UserIdentity userIdentity;
  182. ProcessJob( final UserIdentity userIdentity )
  183. {
  184. this.userIdentity = userIdentity;
  185. }
  186. @Override
  187. public void run()
  188. {
  189. try
  190. {
  191. processUserIdentity( userIdentity );
  192. debugOutputTask.conditionallyExecuteTask();
  193. }
  194. catch ( final Exception e )
  195. {
  196. LOGGER.trace( () -> "unexpected error processing user '" + userIdentity.toDisplayString() + "', error: " + e.getMessage() );
  197. }
  198. }
  199. }
  200. private void processUserIdentity(
  201. final UserIdentity userIdentity
  202. )
  203. throws PwmUnrecoverableException
  204. {
  205. if ( !canRunOnThisServer() || cancelFlag.get() )
  206. {
  207. return;
  208. }
  209. examinedCount.incrementAndGet();
  210. final ChaiUser theUser = pwmApplication.getProxiedChaiUser( userIdentity );
  211. final Instant passwordExpirationTime = LdapOperationsHelper.readPasswordExpirationTime( theUser );
  212. if ( passwordExpirationTime == null )
  213. {
  214. LOGGER.trace( SESSION_LABEL, () -> "skipping user '" + userIdentity.toDisplayString() + "', has no password expiration" );
  215. return;
  216. }
  217. if ( passwordExpirationTime.isBefore( Instant.now() ) )
  218. {
  219. LOGGER.trace( SESSION_LABEL, () -> "skipping user '" + userIdentity.toDisplayString() + "', password expiration is in the past" );
  220. return;
  221. }
  222. final int nextDayInterval = figureNextDayInterval( passwordExpirationTime );
  223. if ( nextDayInterval < 1 )
  224. {
  225. LOGGER.trace( SESSION_LABEL, () -> "skipping user '" + userIdentity.toDisplayString() + "', password expiration time is not within an interval" );
  226. return;
  227. }
  228. if ( checkIfNoticeAlreadySent( userIdentity, passwordExpirationTime, nextDayInterval ) )
  229. {
  230. log( "notice for interval " + nextDayInterval + " already sent for " + userIdentity.toDisplayString() );
  231. return;
  232. }
  233. log( "sending notice to " + userIdentity.toDisplayString() + " for interval " + nextDayInterval );
  234. storageService.writeStoredUserState( userIdentity, SESSION_LABEL, new PwNotifyUserStatus( passwordExpirationTime, Instant.now(), nextDayInterval ) );
  235. sendNoticeEmail( userIdentity );
  236. }
  237. private int figureNextDayInterval(
  238. final Instant passwordExpirationTime
  239. )
  240. {
  241. final long maxSecondsAfterExpiration = TimeDuration.DAY.as( TimeDuration.Unit.SECONDS );
  242. int nextDayInterval = -1;
  243. for ( final int configuredDayInterval : settings.getNotificationIntervals() )
  244. {
  245. final Instant futureConfiguredDayInterval = Instant.now().plus( configuredDayInterval, ChronoUnit.DAYS );
  246. final long secondsUntilConfiguredInterval = Duration.between( Instant.now(), futureConfiguredDayInterval ).abs().getSeconds();
  247. final long secondsUntilPasswordExpiration = Duration.between( Instant.now(), passwordExpirationTime ).abs().getSeconds();
  248. if ( secondsUntilPasswordExpiration < secondsUntilConfiguredInterval )
  249. {
  250. final long secondsBetweenIntervalAndExpiration = Duration.between( futureConfiguredDayInterval, passwordExpirationTime ).abs().getSeconds();
  251. if ( secondsBetweenIntervalAndExpiration < maxSecondsAfterExpiration )
  252. {
  253. nextDayInterval = configuredDayInterval;
  254. }
  255. }
  256. }
  257. return nextDayInterval;
  258. }
  259. private boolean checkIfNoticeAlreadySent(
  260. final UserIdentity userIdentity,
  261. final Instant passwordExpirationTime,
  262. final int interval
  263. )
  264. throws PwmUnrecoverableException
  265. {
  266. final Optional<PwNotifyUserStatus> optionalStoredState = storageService.readStoredUserState( userIdentity, SESSION_LABEL );
  267. if ( !optionalStoredState.isPresent() )
  268. {
  269. return false;
  270. }
  271. final PwNotifyUserStatus storedState = optionalStoredState.get();
  272. if ( storedState.getExpireTime() == null || !storedState.getExpireTime().equals( passwordExpirationTime ) )
  273. {
  274. return false;
  275. }
  276. if ( storedState.getInterval() == 0 || storedState.getInterval() != interval )
  277. {
  278. return false;
  279. }
  280. return true;
  281. }
  282. private void sendNoticeEmail( final UserIdentity userIdentity )
  283. throws PwmUnrecoverableException
  284. {
  285. final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxyForOfflineUser(
  286. pwmApplication,
  287. SESSION_LABEL,
  288. userIdentity
  289. );
  290. final Locale ldapLocale = LocaleHelper.parseLocaleString( userInfoBean.getLanguage() );
  291. final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, ldapLocale, SESSION_LABEL, userIdentity );
  292. final EmailItemBean emailItemBean = pwmApplication.getConfig().readSettingAsEmail(
  293. PwmSetting.EMAIL_PW_EXPIRATION_NOTICE,
  294. ldapLocale
  295. );
  296. noticeCount.incrementAndGet();
  297. StatisticsManager.incrementStat( pwmApplication, Statistic.PWNOTIFY_EMAILS_SENT );
  298. pwmApplication.getEmailQueue().submitEmail( emailItemBean, userInfoBean, macroMachine );
  299. }
  300. private void log( final String output )
  301. {
  302. final String msg = JavaHelper.toIsoDate( Instant.now() )
  303. + " "
  304. + output
  305. + "\n";
  306. if ( debugWriter != null )
  307. {
  308. try
  309. {
  310. debugWriter.append( msg );
  311. debugWriter.flush();
  312. }
  313. catch ( final IOException e )
  314. {
  315. LOGGER.warn( SessionLabel.PWNOTIFY_SESSION_LABEL, () -> "unexpected IO error writing to debugWriter: " + e.getMessage() );
  316. }
  317. }
  318. internalLog.append( msg );
  319. while ( internalLog.length() > MAX_LOG_SIZE )
  320. {
  321. final int nextLf = internalLog.indexOf( "\n" );
  322. if ( nextLf > 0 )
  323. {
  324. internalLog.delete( 0, nextLf );
  325. }
  326. else
  327. {
  328. internalLog.delete( 0, Math.max( 1024, internalLog.length() ) );
  329. }
  330. }
  331. LOGGER.trace( SessionLabel.PWNOTIFY_SESSION_LABEL, () -> output );
  332. }
  333. private ThreadPoolExecutor createExecutor( final PwmApplication pwmApplication )
  334. {
  335. final ThreadFactory threadFactory = PwmScheduler.makePwmThreadFactory( PwmScheduler.makeThreadName( pwmApplication, this.getClass() ), true );
  336. final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
  337. 1,
  338. 10,
  339. 1,
  340. TimeUnit.MINUTES,
  341. new LinkedBlockingDeque<>(),
  342. threadFactory
  343. );
  344. threadPoolExecutor.allowCoreThreadTimeOut( true );
  345. return threadPoolExecutor;
  346. }
  347. }