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

openmrs / openmrs-core / 28515008125

01 Jul 2026 11:43AM UTC coverage: 63.785% (+0.1%) from 63.651%
28515008125

push

github

rkorytkowski
Fix @since 2.9.x to 2.9.0

24079 of 37750 relevant lines covered (63.79%)

0.64 hits per line

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

88.41
/api/src/main/java/org/openmrs/api/context/UserContext.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 org.apache.commons.lang3.StringUtils;
13
import org.openmrs.Location;
14
import org.openmrs.PrivilegeListener;
15
import org.openmrs.Role;
16
import org.openmrs.User;
17
import org.openmrs.UserSessionListener;
18
import org.openmrs.UserSessionListener.Event;
19
import org.openmrs.UserSessionListener.Status;
20
import org.openmrs.api.APIAuthenticationException;
21
import org.openmrs.api.LocationService;
22
import org.openmrs.util.LocaleUtility;
23
import org.openmrs.util.OpenmrsConstants;
24
import org.openmrs.util.RoleConstants;
25
import org.slf4j.Logger;
26
import org.slf4j.LoggerFactory;
27

28
import java.io.Serializable;
29
import java.util.ArrayList;
30
import java.util.Arrays;
31
import java.util.Collections;
32
import java.util.HashSet;
33
import java.util.List;
34
import java.util.Locale;
35
import java.util.Objects;
36
import java.util.Set;
37

38
/**
39
 * Represents an OpenMRS <code>User Context</code> which stores the current user information. Only
40
 * one <code>User</code> may be authenticated within a UserContext at any given time. The
41
 * UserContext should not be accessed directly, but rather used through the <code>Context</code>.
42
 * This class should be kept light-weight. There is one instance of this class per user that is
43
 * logged into the system.
44
 *
45
 * @see org.openmrs.api.context.Context
46
 */
47
public class UserContext implements Serializable {
48
        
49
        private static final long serialVersionUID = -806631231941890648L;
50
        
51
        /**
52
         * Logger - shared by entire class
53
         */
54
        private static final Logger log = LoggerFactory.getLogger(UserContext.class);
1✔
55
        
56
        /**
57
         * User object containing details about the authenticated user
58
         */
59
        private User user = null;
1✔
60
        
61
        /**
62
         * User's permission proxies
63
         */
64
        private final List<String> proxies = Collections.synchronizedList(new ArrayList<>());
1✔
65
        
66
        /**
67
         * User's locale
68
         */
69
        private Locale locale = null;
1✔
70
        
71
        /**
72
         * Cached Role given to all authenticated users
73
         */
74
        private Role authenticatedRole = null;
1✔
75
        
76
        /**
77
         * Cache Role given to all users
78
         */
79
        private Role anonymousRole = null;
1✔
80
        
81
        /**
82
         * User's defined location
83
         */
84
        private Integer locationId;
85
        
86
        /**
87
         * The authentication scheme for this user
88
         */
89
        private final AuthenticationScheme authenticationScheme;
90
        
91
        /**
92
         * Creates a user context based on the provided auth. scheme.
93
         *
94
         * @param authenticationScheme The auth. scheme that applies for this user context.
95
         * @since 2.3.0
96
         */
97
        public UserContext(AuthenticationScheme authenticationScheme) {
1✔
98
                this.authenticationScheme = authenticationScheme;
1✔
99
        }
1✔
100
        
101
        /**
102
         * Authenticate user with the provided credentials. The authentication scheme must be Spring wired, see {@link Context#getAuthenticationScheme()}.
103
         *
104
         * @param credentials The credentials to use to authenticate
105
         * @return The authenticated client information
106
         * @throws ContextAuthenticationException if authentication fails
107
         * @since 2.3.0
108
         */
109
        public Authenticated authenticate(Credentials credentials)
110
                throws ContextAuthenticationException {
111
                
112
                log.debug("Authenticating client '{}' with scheme '{}'", credentials.getClientName(),
1✔
113
                        credentials.getAuthenticationScheme());
1✔
114
                
115
                Authenticated authenticated = null;
1✔
116
                try {
117
                        authenticated = authenticationScheme.authenticate(credentials);
1✔
118
                        this.user = authenticated.getUser();
1✔
119
                        notifyUserSessionListener(this.user, Event.LOGIN, Status.SUCCESS);
1✔
120
                }
121
                catch (ContextAuthenticationException e) {
1✔
122
                        User loggingInUser = new User();
1✔
123
                        loggingInUser.setUsername(credentials.getClientName());
1✔
124
                        notifyUserSessionListener(loggingInUser, Event.LOGIN, Status.FAIL);
1✔
125
                        throw e;
1✔
126
                }
1✔
127
                
128
                setUserLocation(true);
1✔
129
                setUserLocale(true);
1✔
130
                
131
                log.debug("Authenticated as: {}", this.user);
1✔
132
                
133
                return authenticated;
1✔
134
        }
135
        
136
        /**
137
         * Refresh the authenticated user object in this UserContext. This should be used when updating
138
         * information in the database about the current user and it needs to be reflecting in the
139
         * (cached) {@link #getAuthenticatedUser()} User object.
140
         *
141
         * @since 1.5
142
         */
143
        public void refreshAuthenticatedUser() {
144
                log.debug("Refreshing authenticated user");
1✔
145
                
146
                if (user != null) {
1✔
147
                        user = Context.getUserService().getUser(user.getUserId());
1✔
148
                        //update the stored location in the user's session
149
                        setUserLocation(false);
1✔
150
                        setUserLocale(false);
1✔
151
                }
152
        }
1✔
153
        
154
        /**
155
         * Change current authentication to become another user. (You can only do this if you're already
156
         * authenticated as a superuser.)
157
         *
158
         * @param systemId
159
         * @return The new user that this context has been set to. (null means no change was made)
160
         * @throws ContextAuthenticationException
161
         */
162
        public User becomeUser(String systemId) throws ContextAuthenticationException {
163
                if (!Daemon.isDaemonThread() && !Context.getAuthenticatedUser().isSuperUser()) {
1✔
164
                        throw new APIAuthenticationException("You must be a superuser to assume another user's identity");
×
165
                }
166
                
167
                log.debug("Turning the authenticated user into user with systemId: {}", systemId);
1✔
168
                
169
                User userToBecome = Context.getUserService().getUserByUsername(systemId);
1✔
170
                
171
                if (userToBecome == null) {
1✔
172
                        throw new ContextAuthenticationException("User not found with systemId: " + systemId);
1✔
173
                }
174
                
175
                // hydrate the user object
176
                if (userToBecome.getAllRoles() != null) {
1✔
177
                        userToBecome.getAllRoles().size();
1✔
178
                }
179
                
180
                if (userToBecome.getUserProperties() != null) {
1✔
181
                        userToBecome.getUserProperties().size();
1✔
182
                }
183
                
184
                if (userToBecome.getPrivileges() != null) {
1✔
185
                        userToBecome.getPrivileges().size();
1✔
186
                }
187
                
188
                this.user = userToBecome;
1✔
189
                
190
                //update the user's location and locale
191
                setUserLocation(false);
1✔
192
                setUserLocale(false);
1✔
193
                
194
                log.debug("Becoming user: {}", user);
1✔
195
                
196
                return userToBecome;
1✔
197
        }
198
        
199
        /**
200
         * @return "active" user who has been authenticated, otherwise <code>null</code>
201
         */
202
        public User getAuthenticatedUser() {
203
                return user;
1✔
204
        }
205
        
206
        /**
207
         * @return true if user has been authenticated in this UserContext
208
         */
209
        public boolean isAuthenticated() {
210
                return user != null;
1✔
211
        }
212
        
213
        /**
214
         * logs out the "active" (authenticated) user within this UserContext
215
         *
216
         * @see #authenticate
217
         */
218
        public void logout() {
219
                log.debug("setting user to null on logout");
1✔
220
                notifyUserSessionListener(user, Event.LOGOUT, Status.SUCCESS);
1✔
221
                user = null;
1✔
222
                locationId = null;
1✔
223
                locale = null;
1✔
224
                proxies.clear();
1✔
225
        }
1✔
226
        
227
        /**
228
         * Gives the given privilege to all calls to hasPrivilege. This method was visualized as being
229
         * used as follows (try/finally is important):
230
         *
231
         * <pre>
232
         * try {
233
         *   Context.addProxyPrivilege(&quot;AAA&quot;);
234
         *   Context.get*Service().methodRequiringAAAPrivilege();
235
         * }
236
         * finally {
237
         *   Context.removeProxyPrivilege(&quot;AAA&quot;);
238
         * }
239
         * </pre>
240
         *
241
         * @param privilege to give to users
242
         */
243
        public void addProxyPrivilege(String privilege) {
244
                addProxyPrivilege(new String[] {privilege});
1✔
245
        }
1✔
246
        
247
        /**
248
         * Will remove one instance of privilege from the privileges that are currently proxied
249
         *
250
         * @param privilege Privilege to remove in string form
251
         */
252
        public void removeProxyPrivilege(String privilege) {
253
                removeProxyPrivilege(new String[] {privilege});
1✔
254
        }
1✔
255
        
256
        /**
257
         * Adds one or more privileges to the list of privileges that that {@link #hasPrivilege(String)} will
258
         * regard as available regardless of whether the user would otherwise have the privilege.
259
         * <p/>
260
         * This is useful for situations where a system process may need access to some piece of data that the
261
         * user would not otherwise have access to, like a GlobalProperty. <strong>This facility should not be
262
         * used to return data to the user that they otherwise would be unable to see.</strong>
263
         * <p/>
264
         * The expected usage is:
265
         * <p/>
266
         * <pre>{@code
267
         * try {
268
         *   Context.addProxyPrivilege(&quot;AAA&quot;);
269
         *   Context.get*Service().methodRequiringAAAPrivilege();
270
         * }
271
         * finally {
272
         *   Context.removeProxyPrivilege(&quot;AAA&quot;);
273
         * }}
274
         * </pre>
275
         * <p/>
276
         *
277
         * @param privileges privileges to add in string form
278
         * @see #hasPrivilege(String)
279
         * @see #removeProxyPrivilege(String...)
280
         */
281
        public void addProxyPrivilege(String... privileges) {
282
                if (privileges == null || Arrays.stream(privileges).anyMatch(Objects::isNull)) {
1✔
283
                        throw new IllegalArgumentException("UserContext.addProxyPrivilege does not accept null privileges");
1✔
284
                }
285
                
286
                for (String privilege : privileges) {
1✔
287
                        log.debug("Adding proxy privilege: {}", privilege);
1✔
288
                        proxies.add(privilege);
1✔
289
                }
290
        }
1✔
291
        
292
        /**
293
         * Removes one or more privileges to the list of privileges that that {@link #hasPrivilege(String)} will
294
         * regard as available regardless of whether the user would otherwise have the privilege.
295
         * <p/>
296
         * This is the compliment for {@link #addProxyPrivilege(String...)} to clean-up the context.
297
         * <p/>
298
         *
299
         * @param privileges privileges to remove in string form
300
         * @see #hasPrivilege(String)
301
         * @see #addProxyPrivilege(String...)
302
         */
303
        public void removeProxyPrivilege(String... privileges) {
304
                if (privileges == null || Arrays.stream(privileges).allMatch(Objects::isNull)) {
1✔
305
                        return;
1✔
306
                }
307
                
308
                for (String privilege : privileges) {
1✔
309
                        if (privilege != null) {
1✔
310
                                log.debug("Removing privilege: {}", privilege);
1✔
311
                                proxies.remove(privilege);
1✔
312
                        }
313
                }
314
        }
1✔
315

316
        /**
317
         * @return true if there are any proxy privileges
318
         * @since 2.9.0
319
         */
320
        public boolean hasProxyPrivileges() {
321
                return !proxies.isEmpty();
1✔
322
        }
323
        
324
        /**
325
         * @param locale new locale for this context
326
         */
327
        public void setLocale(Locale locale) {
328
                this.locale = locale;
1✔
329
        }
1✔
330
        
331
        /**
332
         * @return current locale for this context
333
         */
334
        public Locale getLocale() {
335
                if (locale == null) {
1✔
336
                        // don't cache default locale - allows recognition of changed default at login page
337
                        return LocaleUtility.getDefaultLocale();
1✔
338
                }
339
                
340
                return locale;
1✔
341
        }
342
        
343
        /**
344
         * Gets all the roles for the (un)authenticated user. Anonymous and Authenticated roles are
345
         * appended if necessary
346
         *
347
         * @return all expanded roles for a user
348
         * @throws Exception
349
         */
350
        public Set<Role> getAllRoles() throws Exception {
351
                return getAllRoles(getAuthenticatedUser());
×
352
        }
353
        
354
        /**
355
         * Gets all the roles for a user. Anonymous and Authenticated roles are appended if necessary
356
         *
357
         * @param user
358
         * @return all expanded roles for a user
359
         * <strong>Should</strong> not fail with null user
360
         * <strong>Should</strong> add anonymous role to all users
361
         * <strong>Should</strong> add authenticated role to all authenticated users
362
         * <strong>Should</strong> return same roles as user getAllRoles method
363
         */
364
        public Set<Role> getAllRoles(User user) throws Exception {
365
                Set<Role> roles = new HashSet<>();
×
366
                
367
                // add the Anonymous Role
368
                roles.add(getAnonymousRole());
×
369
                
370
                // add the Authenticated role
371
                if (getAuthenticatedUser() != null && getAuthenticatedUser().equals(user)) {
×
372
                        roles.addAll(user.getAllRoles());
×
373
                        roles.add(getAuthenticatedRole());
×
374
                }
375
                
376
                return roles;
×
377
        }
378
        
379
        /**
380
         * Tests whether the currently authenticated user has a particular privilege
381
         *
382
         * @param privilege The privilege to check if it's available
383
         * @return true if authenticated user has given privilege
384
         * <strong>Should</strong> authorize if authenticated user has specified privilege
385
         * <strong>Should</strong> authorize if authenticated role has specified privilege
386
         * <strong>Should</strong> authorize if proxied user has specified privilege
387
         * <strong>Should</strong> authorize if anonymous user has specified privilege
388
         * <strong>Should</strong> not authorize if authenticated user does not have specified privilege
389
         * <strong>Should</strong> not authorize if authenticated role does not have specified privilege
390
         * <strong>Should</strong> not authorize if proxied user does not have specified privilege
391
         * <strong>Should</strong> not authorize if anonymous user does not have specified privilege
392
         */
393
        public boolean hasPrivilege(String privilege) {
394
                log.debug("Checking '{}' against proxies: {}", privilege, proxies);
1✔
395
                // check proxied privileges
396
                for (String s : new ArrayList<>(proxies)) {
1✔
397
                        if (s.equals(privilege)) {
1✔
398
                                notifyPrivilegeListeners(getAuthenticatedUser(), privilege, true);
1✔
399
                                return true;
1✔
400
                        }
401
                }
1✔
402
                
403
                // if a user has logged in, check their privileges
404
                if (isAuthenticated()
1✔
405
                        && (getAuthenticatedUser().hasPrivilege(privilege) || getAuthenticatedRole().hasPrivilege(privilege))) {
1✔
406
                        
407
                        // check user's privileges
408
                        notifyPrivilegeListeners(getAuthenticatedUser(), privilege, true);
1✔
409
                        return true;
1✔
410
                        
411
                }
412
                
413
                if (getAnonymousRole().hasPrivilege(privilege)) {
1✔
414
                        notifyPrivilegeListeners(getAuthenticatedUser(), privilege, true);
×
415
                        return true;
×
416
                }
417
                
418
                // default return value
419
                notifyPrivilegeListeners(getAuthenticatedUser(), privilege, false);
1✔
420
                return false;
1✔
421
        }
422
        
423
        /**
424
         * Convenience method to get the Role in the system designed to be given to all users
425
         *
426
         * @return Role
427
         * <strong>Should</strong> fail if database doesn't contain anonymous role
428
         */
429
        private Role getAnonymousRole() {
430
                if (anonymousRole != null) {
1✔
431
                        return anonymousRole;
1✔
432
                }
433
                
434
                anonymousRole = Context.getUserService().getRole(RoleConstants.ANONYMOUS);
1✔
435
                if (anonymousRole == null) {
1✔
436
                        throw new RuntimeException(
×
437
                                "Database out of sync with code: " + RoleConstants.ANONYMOUS + " role does not exist");
438
                }
439
                
440
                return anonymousRole;
1✔
441
        }
442
        
443
        /**
444
         * Convenience method to get the Role in the system designed to be given to all users that have
445
         * authenticated in some manner
446
         *
447
         * @return Role
448
         * <strong>Should</strong> fail if database doesn't contain authenticated role
449
         */
450
        private Role getAuthenticatedRole() {
451
                if (authenticatedRole != null) {
1✔
452
                        return authenticatedRole;
1✔
453
                }
454
                
455
                authenticatedRole = Context.getUserService().getRole(RoleConstants.AUTHENTICATED);
1✔
456
                if (authenticatedRole == null) {
1✔
457
                        throw new RuntimeException("Database out of sync with code: " + RoleConstants.AUTHENTICATED
×
458
                                + " role does not exist");
459
                }
460
                
461
                return authenticatedRole;
1✔
462
        }
463
        
464
        /**
465
         * @return locationId for this user context if any is set
466
         * @since 1.10
467
         */
468
        public Integer getLocationId() {
469
                return locationId;
×
470
        }
471
        
472
        /**
473
         * @param locationId locationId to set
474
         * @since 1.10
475
         */
476
        public void setLocationId(Integer locationId) {
477
                this.locationId = locationId;
1✔
478
        }
1✔
479
        
480
        /**
481
         * @return current location for this user context if any is set
482
         * @since 1.9
483
         */
484
        public Location getLocation() {
485
                if (locationId == null) {
1✔
486
                        return null;
1✔
487
                }
488
                return Context.getLocationService().getLocation(locationId);
1✔
489
        }
490
        
491
        /**
492
         * @param location the location to set to
493
         * @since 1.9
494
         */
495
        public void setLocation(Location location) {
496
                if (location != null) {
1✔
497
                        this.locationId = location.getLocationId();
1✔
498
                }
499
        }
1✔
500
        
501
        /**
502
         * Convenience method that sets the default location of the currently authenticated user using
503
         * the value of the user's default location property
504
         */
505
        private void setUserLocation(boolean useDefault) {
506
                // location should be null if no user is logged in
507
                if (this.user == null) {
1✔
508
                        this.locationId = null;
×
509
                        return;
×
510
                }
511
                
512
                // intended to be when the user initially authenticates
513
                if (this.locationId == null && useDefault) {
1✔
514
                        this.locationId = getDefaultLocationId(this.user);
1✔
515
                }
516
        }
1✔
517

518
        /**
519
         * Convenience method that sets the default locale used by the currently authenticated user, using
520
         * the value of the user's default local property
521
         */
522
        private void setUserLocale(boolean useDefault) {
523
                // local should be null if no user is logged in
524
                if (this.user == null) {
1✔
525
                        this.locale = null;
×
526
                        return;
×
527
                }
528

529
                // intended to be when the user initially authenticates
530
                if (user.getUserProperties().containsKey("defaultLocale")) {
1✔
531
                        String localeString = user.getUserProperty("defaultLocale");
1✔
532
                        locale = LocaleUtility.fromSpecification(localeString);
1✔
533
                }
534

535
                if (locale == null && useDefault) {
1✔
536
                        locale = LocaleUtility.getDefaultLocale();
1✔
537
                }
538

539
        }
1✔
540
        
541
        protected Integer getDefaultLocationId(User user) {
542
                String defaultLocation = user.getUserProperty(OpenmrsConstants.USER_PROPERTY_DEFAULT_LOCATION);
1✔
543
                if (StringUtils.isNotBlank(defaultLocation)) {
1✔
544
                        LocationService ls = Context.getLocationService();
1✔
545
                        //only go ahead if it has actually changed OR if wasn't set before
546
                        try {
547
                                int defaultId = Integer.parseInt(defaultLocation);
1✔
548
                                if (this.locationId == null || this.locationId != defaultId) {
1✔
549
                                        // validate that the id is a valid id
550
                                        if (ls.getLocation(defaultId) != null) {
1✔
551
                                                return defaultId;
1✔
552
                                        }
553
                                }
554
                        }
555
                        catch (NumberFormatException ignored) {
1✔
556
                        }
1✔
557

558
                        Location possibleLocation = ls.getLocationByUuid(defaultLocation);
1✔
559

560
                        if (possibleLocation != null && (this.locationId == null || !this.locationId.equals(possibleLocation.getId()))) {
1✔
561
                                return possibleLocation.getId();
1✔
562
                        }
563

564
                        log.warn("The default location for user '{}' is set to '{}', which is not a valid location",
1✔
565
                                user.getUsername(), defaultLocation);
1✔
566
                }
567
                
568
                return null;
1✔
569
        }
570
        
571
        /**
572
         * Notifies privilege listener beans about any privilege check.
573
         * <p>
574
         * It is called by {@link UserContext#hasPrivilege(java.lang.String)}.
575
         *
576
         * @param user         the authenticated user or <code>null</code> if not authenticated
577
         * @param privilege    the checked privilege
578
         * @param hasPrivilege <code>true</code> if the authenticated user has the required privilege or
579
         *                     if it is a proxy privilege
580
         * @see PrivilegeListener
581
         * @since 1.8.4, 1.9.1, 1.10
582
         */
583
        private void notifyPrivilegeListeners(User user, String privilege, boolean hasPrivilege) {
584
                for (PrivilegeListener privilegeListener : Context.getRegisteredComponents(PrivilegeListener.class)) {
1✔
585
                        try {
586
                                privilegeListener.privilegeChecked(user, privilege, hasPrivilege);
1✔
587
                        }
588
                        catch (Exception e) {
×
589
                                log.error("Privilege listener has failed", e);
×
590
                        }
1✔
591
                }
1✔
592
        }
1✔
593
        
594
        private void notifyUserSessionListener(User user, Event event, Status status) {
595
                for (UserSessionListener userSessionListener : Context.getRegisteredComponents(UserSessionListener.class)) {
1✔
596
                        userSessionListener.loggedInOrOut(user, event, status);
1✔
597
                }
1✔
598
        }
1✔
599
}
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