• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmrs / openmrs-core / 18321298560

07 Oct 2025 05:43PM UTC coverage: 65.263% (-0.001%) from 65.264%
18321298560

push

github

web-flow
TRUNK-6435: Password resets must go through service API (#5379)

58 of 76 new or added lines in 3 files covered. (76.32%)

6 existing lines in 5 files now uncovered.

23689 of 36298 relevant lines covered (65.26%)

0.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.06
/api/src/main/java/org/openmrs/api/context/Daemon.java
1
/**
2
 * This Source Code Form is subject to the terms of the Mozilla Public License,
3
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
4
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6
 *
7
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8
 * graphic logo is a trademark of OpenMRS Inc.
9
 */
10
package org.openmrs.api.context;
11

12
import java.util.List;
13
import java.util.concurrent.Callable;
14
import java.util.concurrent.CountDownLatch;
15
import java.util.concurrent.ExecutionException;
16
import java.util.concurrent.Future;
17
import java.util.stream.Collectors;
18

19
import org.apache.commons.collections.CollectionUtils;
20
import org.openmrs.Role;
21
import org.openmrs.User;
22
import org.openmrs.api.APIAuthenticationException;
23
import org.openmrs.api.APIException;
24
import org.openmrs.api.OpenmrsService;
25
import org.openmrs.api.db.ContextDAO;
26
import org.openmrs.api.db.hibernate.HibernateContextDAO;
27
import org.openmrs.module.DaemonToken;
28
import org.openmrs.module.Module;
29
import org.openmrs.module.ModuleException;
30
import org.openmrs.module.ModuleFactory;
31
import org.openmrs.scheduler.Task;
32
import org.openmrs.scheduler.timer.TimerSchedulerTask;
33
import org.openmrs.util.OpenmrsThreadPoolHolder;
34
import org.springframework.context.support.AbstractRefreshableApplicationContext;
35

36
/**
37
 * This class allows certain tasks to run with elevated privileges. Primary use is scheduling and
38
 * module startup when there is no user to authenticate as.
39
 */
40
public final class Daemon {
41
        
42
        /**
43
         * The uuid defined for the daemon user object
44
         */
45
        static final String DAEMON_USER_UUID = "A4F30A1B-5EB9-11DF-A648-37A07F9C90FB";
46
        
47
        private static final ThreadLocal<Boolean> isDaemonThread = new ThreadLocal<>();
1✔
48
        
49
        private static final ThreadLocal<User> daemonThreadUser = new ThreadLocal<>();
1✔
50
        
51
        /**
52
         * private constructor to override the default constructor to prevent it from being instantiated.
53
         */
54
        private Daemon() {
55
        }
56
        
57
        /**
58
         * @see #startModule(Module, boolean, AbstractRefreshableApplicationContext)
59
         */
60
        public static Module startModule(Module module) throws ModuleException {
61
                return startModule(module, false, null);
×
62
        }
63
        
64
        /**
65
         * This method should not be called directly. The {@link ModuleFactory#startModule(Module)}
66
         * method uses this to start the given module in a new thread that is authenticated as the
67
         * daemon user. <br>
68
         * If a non-null application context is passed in, it gets refreshed to make the module's
69
         * services available
70
         *
71
         * @param module the module to start
72
         * @param isOpenmrsStartup Specifies whether this module is being started at application startup
73
         *            or not
74
         * @param applicationContext the spring application context instance to refresh
75
         * @return the module returned from {@link ModuleFactory#startModuleInternal(Module)}
76
         */
77
        public static Module startModule(final Module module, final boolean isOpenmrsStartup,
78
                final AbstractRefreshableApplicationContext applicationContext) throws ModuleException {
79
                StackTraceElement[] stack = new Exception().getStackTrace();
1✔
80
                if (stack.length < 2) {
1✔
NEW
81
                        throw new APIException("Could not determine where startModule() was called from");
×
82
                }
83
                StackTraceElement caller = stack[1];
1✔
84
                String callerClass = caller.getClassName();
1✔
85
                
86
                if (!Daemon.class.getName().equals(callerClass) && !ModuleFactory.class.getName().equals(callerClass)) {
1✔
NEW
87
                        throw new APIException("Module.factory.only", new Object[] { callerClass });
×
88
                }
89
                
90
                Future<Module> moduleStartFuture = runInDaemonThreadInternal(() -> ModuleFactory.startModuleInternal(module, isOpenmrsStartup, applicationContext));
1✔
91
                
92
                // wait for the "startModule" thread to finish
93
                try {
94
                        return moduleStartFuture.get();
1✔
95
                }
96
                catch (InterruptedException e) {
×
97
                        // ignore
98
                } catch (ExecutionException e) {
×
99
                        if (e.getCause() instanceof ModuleException) {
×
100
                                throw (ModuleException) e.getCause();
×
101
                        } else {
102
                                throw new ModuleException("Unable to start module " + module.getName(), e);
×
103
                        }
104
                }
×
105

106
                return module;
×
107
        }
108

109
        /**
110
         * This method should not be called directly, only {@link ContextDAO#createUser(User, String, List)} can
111
         * legally invoke this method.
112
         * 
113
         * @param user A new user to be created.
114
         * @param password The password to set for the new user.
115
         * @param roleNames A list of role names to fetch the roles to add to the user.
116
         * @return The newly created user
117
         * 
118
         * <strong>Should</strong> only allow the creation of new users, not the edition of existing ones
119
         * 
120
         * @since 2.3.0
121
         */
122
        public static User createUser(User user, String password, List<String> roleNames) throws Exception {
123
                // quick check to make sure we're only being called by ourselves
124
                StackTraceElement[] stack = new Exception().getStackTrace();
1✔
125
                if (stack.length < 2) {
1✔
NEW
126
                        throw new APIException("Could not determine where createUser() was called from");
×
127
                }
128
                StackTraceElement caller = stack[1];
1✔
129
                String callerClass = caller.getClassName();
1✔
130
                
131
                if (!HibernateContextDAO.class.getName().equals(callerClass)) {
1✔
132
                        throw new APIException("Context.DAO.only", new Object[] { callerClass });
1✔
133
                }
134

135
                // create a new thread and execute that task in it
136
                Future<User> userFuture = runInDaemonThreadInternal(() -> {
1✔
137
                        if ((user.getId() != null && Context.getUserService().getUser(user.getId()) != null) || Context.getUserService().getUserByUuid(user.getUuid()) != null || Context.getUserService().getUserByUsername(user.getUsername()) != null || (user.getEmail() != null && Context.getUserService().getUserByUsernameOrEmail(user.getEmail()) != null) ) {
1✔
138
                                throw new APIException("User.creating.already.exists", new Object[] { user.getDisplayString() });
1✔
139
                        }
140

141
                        if (!CollectionUtils.isEmpty(roleNames)) {
1✔
142
                                List<Role> roles = roleNames.stream().map(roleName -> Context.getUserService().getRole(roleName)).collect(Collectors.toList());
1✔
143
                                roles.forEach(user::addRole);
1✔
144
                        }
145

146
                        return Context.getUserService().createUser(user, password);
1✔
147
                });
148

149
                // wait for the 'create user' thread to finish
150
                try {
151
                        return userFuture.get();
1✔
152
                }
153
                catch (InterruptedException e) {
×
154
                        // ignore
155
                }
156
                catch (ExecutionException e) {
1✔
157
                        if (e.getCause() instanceof Exception) {
1✔
158
                                throw (Exception) e.getCause();
1✔
159
                        } else {
160
                                throw new RuntimeException(e.getCause());
×
161
                        }
162
                }
×
163

164
                return null;
×
165
        }        
166
        
167
        /**
168
         * Executes the given task in a new thread that is authenticated as the daemon user. <br>
169
         * <br>
170
         * This can only be called from {@link TimerSchedulerTask} during actual task execution
171
         *
172
         * @param task the task to run
173
         * <strong>Should</strong> not be called from other methods other than TimerSchedulerTask
174
         * <strong>Should</strong> not throw error if called from a TimerSchedulerTask class
175
         */
176
        public static void executeScheduledTask(final Task task) throws Exception {
177
                // quick check to make sure we're only being called by ourselves
178
                StackTraceElement[] stack = new Exception().getStackTrace();
1✔
179
                if (stack.length < 2) {
1✔
NEW
180
                        throw new APIException("Could not determine where executeScheduledTask() was called from");
×
181
                }
182
                StackTraceElement caller = stack[1];
1✔
183
                String callerClass = caller.getClassName();
1✔
184
                
185
                Class<?> callerClazz;
186
                try {
187
                        callerClazz = Class.forName(callerClass);
1✔
NEW
188
                } catch (ClassNotFoundException e) {
×
NEW
189
                        throw new APIException("Could not determine where executeScheduledTask() was called from", e);
×
190
                }
1✔
191
                
192
                if (!TimerSchedulerTask.class.isAssignableFrom(callerClazz)) {
1✔
193
                        throw new APIException("Scheduler.timer.task.only", new Object[] { callerClass });
1✔
194
                }
195
                
196
                Future<?> scheduleTaskFuture = runInDaemonThreadInternal(() -> TimerSchedulerTask.execute(task));
1✔
197
                
198
                // wait for the "executeTaskThread" thread to finish
199
                try {
200
                        scheduleTaskFuture.get();
1✔
201
                }
202
                catch (InterruptedException e) {
×
203
                        // ignore
204
                } catch (ExecutionException e) {
×
205
                        if (e.getCause() instanceof Exception) {
×
206
                                throw (Exception) e.getCause();
×
207
                        } else {
208
                                throw new RuntimeException(e.getCause());
×
209
                        }
210
                }
1✔
211
        }
1✔
212
        
213
        /**
214
         * Call this method if you are inside a Daemon thread (for example in a Module activator or a
215
         * scheduled task) and you want to start up a new parallel Daemon thread. You may only call this
216
         * method from a Daemon thread.
217
         *
218
         * @param runnable what to run in a new thread
219
         * @return the newly spawned {@link Thread}
220
         * @deprecated As of 2.7.0, consider using {@link #runNewDaemonTask(Runnable)} instead
221
         */
222
        @Deprecated
223
        public static Thread runInNewDaemonThread(final Runnable runnable) {
224
                // make sure we're already in a daemon thread
225
                if (!isDaemonThread()) {
1✔
226
                        throw new APIAuthenticationException("Only daemon threads can spawn new daemon threads");
1✔
227
                }
228

229
                // the previous implementation ensured that Thread.start() was called before this function returned
230
                // since we cannot guarantee that the executor will run the thread when `execute()` is called, we need another
231
                // mechanism to ensure the submitted Runnable was actually started.
232
                final CountDownLatch countDownLatch = new CountDownLatch(1);
1✔
233
                
234
                // we should consider making DaemonThread public, so the caller can access returnedObject and exceptionThrown
235
                DaemonThread thread = new DaemonThread() {
1✔
236
                        
237
                        @Override
238
                        public void run() {
239
                                isDaemonThread.set(true);
1✔
240
                                try {
241
                                        Context.openSession();
1✔
242
                                        countDownLatch.countDown();
1✔
243
                                        //Suppressing sonar issue "squid:S1217"
244
                                        //We intentionally do not start a new thread yet, rather wrap the run call in a session.
245
                                        runnable.run();
1✔
246
                                }
247
                                finally {
248
                                        try {
249
                                                Context.closeSession();
1✔
250
                                        } finally {
251
                                                isDaemonThread.remove();
1✔
252
                                                daemonThreadUser.remove();
1✔
253
                                        }
254
                                }
255
                        }
1✔
256
                };
257
                
258
                OpenmrsThreadPoolHolder.threadExecutor.execute(thread);
1✔
259

260
                // do not return until the thread is actually started to emulate the previous behaviour
261
                try {
262
                        countDownLatch.await();
1✔
263
                } catch (InterruptedException ignored) {
×
264
                }
1✔
265

266
                return thread;
1✔
267
        }
268

269
        /**
270
         * Call this method if you are inside a Daemon thread (for example in a Module activator or a
271
         * scheduled task) and you want to start up a new parallel Daemon thread. You may only call this
272
         * method from a Daemon thread.
273
         *
274
         * @param callable what to run in a new thread
275
         * @return a future that completes when the task is done;
276
         * @since 2.7.0
277
         */
278
        @SuppressWarnings({"squid:S1217", "unused"})
279
        public static <T> Future<T> runInNewDaemonThread(final Callable<T> callable) {
280
                // make sure we're already in a daemon thread
281
                if (!isDaemonThread()) {
1✔
282
                        throw new APIAuthenticationException("Only daemon threads can spawn new daemon threads");
1✔
283
                }
284

285
                return runInDaemonThreadInternal(callable);
1✔
286
        }
287

288
        /**
289
         * Call this method if you are inside a Daemon thread (for example in a Module activator or a
290
         * scheduled task) and you want to start up a new parallel Daemon thread. You may only call this
291
         * method from a Daemon thread.
292
         *
293
         * @param runnable what to run in a new thread
294
         * @return a future that completes when the task is done;
295
         * @since 2.7.0
296
         */
297
        @SuppressWarnings({"squid:S1217", "unused"})
298
        public static Future<?> runNewDaemonTask(final Runnable runnable) {
299
                // make sure we're already in a daemon thread
300
                if (!isDaemonThread()) {
1✔
301
                        throw new APIAuthenticationException("Only daemon threads can spawn new daemon threads");
1✔
302
                }
303
                
304
                return runInDaemonThreadInternal(runnable);
×
305
        }
306
        
307
        /**
308
         * @return true if the current thread was started by this class and so is a daemon thread that
309
         *         has all privileges
310
         * @see Context#hasPrivilege(String)
311
         */
312
        public static boolean isDaemonThread() {
313
                Boolean b = isDaemonThread.get();
1✔
314
                if (b == null || !b) {
1✔
315
                        // Allow functions in Daemon and WebDaemon to be treated as a DaemonThread
316
                        StackTraceElement[] stack = new Exception().getStackTrace();
1✔
317
                        if (stack.length < 3) {
1✔
NEW
318
                                throw new APIException("Could not determine where change password was called from");
×
319
                        }
320
                        StackTraceElement caller = stack[2];
1✔
321
                        String callerClass = caller.getClassName();
1✔
322
                        
323
                        return Daemon.class.getName().equals(callerClass) || "org.openmrs.web.WebDaemon".equals(callerClass);
1✔
324
                } else {
325
                        return true;
1✔
326
                }
327
        }
328
        
329
        /**
330
         * Calls the {@link OpenmrsService#onStartup()} method, as a daemon, for an instance
331
         * implementing the {@link OpenmrsService} interface.
332
         *
333
         * @param service instance implementing the {@link OpenmrsService} interface.
334
         * @since 1.9
335
         */
336
        public static void runStartupForService(final OpenmrsService service) throws ModuleException {
337
                StackTraceElement[] stack = new Exception().getStackTrace();
1✔
338
                if (stack.length < 2) {
1✔
NEW
339
                        throw new APIException("Could not determine where change password was called from");
×
340
                }
341
                StackTraceElement caller = stack[1];
1✔
342
                String callerClass = caller.getClassName();
1✔
343
                
344
                if (!ServiceContext.class.getName().equals(callerClass)) {
1✔
NEW
345
                        throw new APIException("Service.context.only", new Object[] { callerClass });
×
346
                }
347
                
348
                Future<?> future = runInDaemonThreadInternal(service::onStartup);
1✔
349
                
350
                // wait for the "onStartup" thread to finish
351
                try {
352
                        future.get();
1✔
353
                }
354
                catch (InterruptedException e) {
×
355
                        // ignore
356
                }
357
                catch (ExecutionException e) {
×
358
                        if (e.getCause() instanceof ModuleException) {
×
359
                                throw (ModuleException) e.getCause();
×
360
                        } else {
361
                                throw new ModuleException("Unable to run onStartup() method of service {}", service.getClass().getSimpleName(), e);
×
362
                        }
363
                }
1✔
364
        }
1✔
365
        
366
        /**
367
         * Executes the given runnable in a new thread that is authenticated as the daemon user.
368
         *
369
         * @param runnable an object implementing the {@link Runnable} interface.
370
         * @param token the token required to run code as the daemon user
371
         * @return the newly spawned {@link Thread}
372
         * @since 1.9.2
373
         * @deprecated Since 2.7.0 use {@link #runInDaemonThreadWithoutResult(Runnable, DaemonToken)} instead
374
         */
375
        @Deprecated
376
        @SuppressWarnings({"squid:S1217", "unused"})
377
        public static Thread runInDaemonThread(final Runnable runnable, DaemonToken token) {
378
                if (!ModuleFactory.isTokenValid(token)) {
×
379
                        throw new ContextAuthenticationException("Invalid token " + token);
×
380
                }
381
                
382
                DaemonThread thread = new DaemonThread() {
×
383
                        @Override
384
                        public void run() {
385
                                isDaemonThread.set(true);
×
386
                                try {
387
                                        Context.openSession();
×
388
                                        //Suppressing sonar issue "squid:S1217"
389
                                        //We intentionally do not start a new thread yet, rather wrap the run call in a session.
390
                                        runnable.run();
×
391
                                }
392
                                finally {
393
                                        try {
394
                                                Context.closeSession();
×
395
                                        } finally {
396
                                                isDaemonThread.remove();
×
397
                                                daemonThreadUser.remove();
×
398
                                        }
399
                                }
400
                        }
×
401
                };
402

403
                OpenmrsThreadPoolHolder.threadExecutor.execute(thread);
×
404
                return thread;
×
405
        }
406

407
        /**
408
         * Executes the given runnable in a new thread that is authenticated as the daemon user.
409
         *
410
         * @param callable an object implementing the {@link Callable<T>} interface to be run
411
         * @param token the token required to run code as the daemon user
412
         * @return the newly spawned {@link Thread}
413
         * @since 2.7.0
414
         */
415
        @SuppressWarnings({"squid:S1217", "unused"})
416
        public static <T> Future<T> runInDaemonThread(final Callable<T> callable, DaemonToken token) {
417
                if (!ModuleFactory.isTokenValid(token)) {
×
418
                        throw new ContextAuthenticationException("Invalid token");
×
419
                }
420
                
421
                return runInDaemonThreadInternal(callable);
×
422
        }
423

424
        /**
425
         * Executes the given runnable in a new thread that is authenticated as the daemon user.
426
         *
427
         * @param runnable an object implementing the {@link Runnable} interface to be run
428
         * @param token the token required to run code as the daemon user
429
         * @return the newly spawned {@link Thread}
430
         * @since 2.7.0
431
         */
432
        @SuppressWarnings("squid:S1217")
433
        public static Future<?> runInDaemonThreadWithoutResult(final Runnable runnable, DaemonToken token) {
434
                if (!ModuleFactory.isTokenValid(token)) {
×
435
                        throw new ContextAuthenticationException("Invalid token");
×
436
                }
437

438
                return runInDaemonThreadInternal(runnable);
×
439
        }
440
        
441
        /**
442
         * Executes the given runnable in a new thread that is authenticated as the daemon user and wait
443
         * for the thread to finish.
444
         *
445
         * @param runnable an object implementing the {@link Runnable} interface.
446
         * @param token the token required to run code as the daemon user
447
         * @since 2.7.0
448
         */
449
        public static void runInDaemonThreadAndWait(final Runnable runnable, DaemonToken token) {
450
                Future<?> daemonThread = runInDaemonThreadWithoutResult(runnable, token);
×
451
                
452
                try {
453
                        daemonThread.get();
×
454
                }
455
                catch (InterruptedException | ExecutionException e) {
×
456
                        // Ignored
457
                }
×
458
        }
×
459
        
460
        private static <T> Future<T> runInDaemonThreadInternal(Callable<T> callable) {
461
                return OpenmrsThreadPoolHolder.threadExecutor.submit(() -> {
1✔
462
                        isDaemonThread.set(true);
1✔
463
                        try {
464
                                Context.openSession();
1✔
465
                                return callable.call();
1✔
466
                        }
467
                        finally {
468
                                try {
469
                                        Context.closeSession();
1✔
470
                                } finally {
471
                                        isDaemonThread.remove();
1✔
472
                                        daemonThreadUser.remove();
1✔
473
                                }
474
                        }
475
                });
476
        }
477
        
478
        private static Future<?> runInDaemonThreadInternal(Runnable runnable) {
479
                // for Threads, we used to guarantee that Thread.start() was called before the function returned
480
                // since we cannot guarantee that the executor actually started executing the thread, we use a CountDownLatch
481
                // to emulate this behaviour when the user submits a Thread. Other runnables are unaffected.
482
                CountDownLatch countDownLatch = getCountDownLatch(runnable instanceof Thread);
1✔
483

484
                Future<?> result = OpenmrsThreadPoolHolder.threadExecutor.submit(() -> {
1✔
485
                        isDaemonThread.set(true);
1✔
486
                        try {
487
                                Context.openSession();
1✔
488
                                countDownLatch.countDown();
1✔
489
                                runnable.run();
1✔
490
                        }
491
                        finally {
492
                                try {
493
                                        Context.closeSession();
1✔
494
                                } finally {
495
                                        isDaemonThread.remove();
1✔
496
                                        daemonThreadUser.remove();
1✔
497
                                }
498
                        }
499
                });
1✔
500
                
501
                try {
502
                        countDownLatch.await();
1✔
503
                } catch (InterruptedException ignored) {}
1✔
504
                
505
                return result;
1✔
506
        }
507

508
        private static CountDownLatch getCountDownLatch(boolean isThread) {
509
                return isThread ? new CountDownLatch(1) : new CountDownLatch(0);
1✔
510
        }
511

512
        /**
513
         * Thread class used by the {@link Daemon#startModule(Module)} and
514
         * {@link Daemon#executeScheduledTask(Task)} methods so that the returned object and the
515
         * exception thrown can be returned to calling class
516
         */
517
        protected static class DaemonThread extends Thread {
1✔
518
                
519
                /**
520
                 * The object returned from the method called in {@link #run()}
521
                 */
522
                protected Object returnedObject = null;
1✔
523
                
524
                /**
525
                 * The exception thrown (if any) by the method called in {@link #run()}
526
                 */
527
                protected Exception exceptionThrown = null;
1✔
528
                
529
                /**
530
                 * Gets the exception thrown (if any) by the method called in {@link #run()}
531
                 *
532
                 * @return the thrown exception (if any).
533
                 */
534
                public Exception getExceptionThrown() {
535
                        return exceptionThrown;
×
536
                }
537
        }
538
        
539
        /**
540
         * Checks whether user is Daemon.
541
         * However, this is not the preferred method for checking to see if the current thread is a daemon thread,
542
         *                                 rather use {@link #isDaemonThread()}.
543
         * isDaemonThread is preferred for checking to see if you are in that thread or if the current thread is daemon.
544
         *
545
         * @param user user whom we are checking if daemon
546
         * @return true if user is Daemon
547
         */
548
        public static boolean isDaemonUser(User user) {
549
                return DAEMON_USER_UUID.equals(user.getUuid());
1✔
550
        }
551
        
552
        /**
553
         * @return the current thread daemon user or null if not assigned
554
         * @since 2.0.0, 1.12.0, 1.11.6, 1.10.4, 1.9.11
555
         */
556
        public static User getDaemonThreadUser() {
557
                if (isDaemonThread()) {
1✔
558
                        User user = daemonThreadUser.get();
1✔
559
                        if (user == null) {
1✔
560
                                user = Context.getContextDAO().getUserByUuid(DAEMON_USER_UUID);
1✔
561
                                daemonThreadUser.set(user);
1✔
562
                        }
563
                        return user;
1✔
564
                } else {
565
                        return null;
×
566
                }
567
        }
568

569
        public static String getDaemonUserUuid() {
570
                return DAEMON_USER_UUID;
1✔
571
        }
572
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc