Last updated Apr 19, 2024

Renamable Users in JIRA 6.0

Introducing the user key

Previously the username field was used both as a display value and also stored as the primary key of the user. In order to allow usernames to be changed, we obviously need a separate identifier field that is unchangeable.

We have introduced a new field called "key" to users that is a case-sensitive String and will never change (also referred to as the "userkey"). In order to correctly support systems with multiple directories, this key is applicable at the application level, not at the user directory level.

Existing users will get allocated a key that is equal to the lowercase of the username. This means there is no need to migrate existing data: the stored value is the user key and even if the user's username is later edited, this stored key will remain correct. (This assumes that you are already lower-casing the stored usernames which was required in order to avoid case-sensitivity bugs).

When to use the key, when to use the username

The userkey should be considered the primary key of the user. Anytime you want to store a reference to a user in long term storage (eg the DB or Lucene index) you should use the key because it can never change.

However, the userkey should never be exposed to the end user; only the username should be displayed. The username is a unique secondary key, but should not be stored in long-term storage because it can change over time.

A brief word on User Directories

Since the introduction of Embedded Crowd as our user management library in v4.3, JIRA is able to connect to multiple User Directories at the same time.
So, for example, a JIRA instance may be connected to an Internal user directory (users stored in the local DB and managed through JIRA's UI) and also an LDAP server.
In this case each directory may have users with the same username.
These "directory level" users are represented by the embedded crowd interface com.atlassian.crowd.embedded.api.User.
User objects will consider users with the same username as different if they have different DirectoryID values.

However, at the application level we can only have one user with a given username.
To amalgamate these, JIRA considers the order of the Directories.
The user in the "higher" directory wins and is used to populate the information for that user account.
The user in the lower directory is effectively ignored; we refer to such an object as a "shadowed user".

A new application-level User object

We have introduced a new Interface to represent users at the Application level called com.atlassian.jira.user.ApplicationUser.
This has some advantages over the directory-level user interface coming directly from the Embedded Crowd library.

  • It includes the user key as a field
    The lower level User object does not know about the key.
  • It has a definition of equals() and hashCode() that is preferable for use in JIRA and plugins.
    Because it only looks at the key value, and does not take the DirectoryID into account.

Note that the ApplicationUser interface was actually added to JIRA as an experimental API in v5.1.1.
Also note that this experimental version extended the directory User interface, but it no longer does in v6 - see the javadoc on ApplicationUser for more information.

ApplicationUser should be considered as a preferred replacement for the embedded crowd User interface.
To get hold of an ApplicationUser object you can call either UserManager.getUserByKey(userKey) or UserManager.getUserByName(userName).

Converting between the User object and the ApplicationUser object

Of course, all of the existing API still allows use of the legacy User interface and you have a bunch of existing code using it that you only want to migrate slowly as required.
We have provided a helper Class - ApplicationUsers - that allows you to quickly convert between the two in a null-safe and performant manner.
See the ApplicationUsers javadoc for more information.

Important Changes to be aware of

All database storage in JIRA will now be using the userkey, not the username.

JIRA API methods that previously accepted or returned a username will in some cases now expect or return a userkey instead - depending on how the method is used.
You should check the API of such methods and consider using alternative methods that take ApplicationUser object when applicable to avoid ambiguity.

Some deprecated API methods that previously returned a directory level User, instead of being deleted, have been replaced to return an ApplicationUser object.

The CustomField.getValue(Issue) method will return ApplicationUser objects instead of User objects for user-based custom fields.
This method is only declared to return Object, so this means taking care when casting. Running old code against JIRA 6.0 may cause following exception:

1
2
java.lang.ClassCastException: com.atlassian.jira.user.DelegatingApplicationUser cannot be cast to com.atlassian.crowd.embedded.api.User

This exception is caused by trying to cast ApplicationUser into User interface. Dealing with such cases is described in next paragraph: Cross Version Compatibility.

Cross Version Compatibility

Developers that want to create plugins that are compatible with JIRA 6 and JIRA 5 have two options:

Option 1 use the v5.x version of the ApplicationUser object

The ApplicationUser object, and some of the supporting methods and classes, were added to JIRA as experimental API in JIRA 5.1.1.
There is one important difference between the 5.x version of ApplicationUser and the 6.x version; namely that in v5.x ApplicationUser was declared to extend the directory-level User interface.

This is problematic because ApplicationUser has incompatible implementations of equals() and hashCode().
The developer must be extra careful not to mix ApplicationUser objects with other User objects that don't implement ApplicationUser, particularly in classes and methods that will call equals() or hashCode().

For this reason, Option 2 is recommended.

Option 2 use the compatibility library

Atlassian provides a compatibility library that your plugin can include so that it can interact with either v5 or v6 in a safe manner.
You can add it as a maven dependency by adding the following to your pom.xml

1
2
<dependency>
  <groupId>com.atlassian.usercompatibility</groupId>
  <artifactId>usercompatibility-jira</artifactId>
</dependency>

See JavaDoc on the helper class UserCompatibilityHelper and it's methods for details.

Caching Errors

Both User and ApplicationUser are representations of mutable data.  It is not safe to store them in a cache, either directly or as a referenced field within another cached object, unless the cache listens for the events that modify user data and invalidate the corresponding cached values.  This is not a new problem, but prior to the ability to rename users, the results of stale users in the cache were mostly cosmetic, such as using the old display name (full name) for a user after it had been updated.

With renamed users, the errors are potentially much more significant.  Depending on what was cached and what changed, it could result in anything from displaying information about the wrong user to unexpected exceptions and even security problems.  To avoid these problems, either keep only the user's key and resolve the user when it is requested or discard cached data for users that are modified.

Unit Tests and the Component Accessor

Existing unit tests may start to throw an IllegalStateException where they did not before with a stack trace like:

1
2
java.lang.IllegalStateException: ComponentAccessor has not been initialised.
    at com.atlassian.jira.component.ComponentAccessor.getWorker(ComponentAccessor.java:843)
    at com.atlassian.jira.component.ComponentAccessor.getComponent(ComponentAccessor.java:106)
    at com.atlassian.jira.component.ComponentAccessor.getUserKeyService(ComponentAccessor.java:209)
    at com.atlassian.jira.user.ApplicationUsers.from(ApplicationUsers.java:39)

This is frequently a problem for unit tests in general, but the code changes in JIRA to support renamed users has made it significantly more common, and plugin developers are likely to encounter it.

Background on the Component Accessor

When a class needs to access another JIRA component, the preferred mechanism to resolve this dependency is by injection in the constructor.  Among other things, this makes it easier to see what the dependencies of the class are and also makes it easy for unit tests to provide mocks for those components.  However, there are times when dependency injection is impossible or impractical.  Examples include: 

  • Components with circular dependencies between them, as one of them must be resolved first without the other dependency available for injection yet.
  • Classes that are not injectable components but are instead explicitly constructed in a long chain of classes that would not otherwise need that component.
  • Static-only utility classes, which are never constructed at all

In these cases, the class can use ComponentAccessor.getComponent(Class) to resolve dependencies.  The drawback is that ComponentAccessor uses a global, static reference to a ComponentAccessor.Worker implementation to accomplish this, and if nothing has initialised that reference, then the IllegalStateException shown above is the result.

As part of the work for allowing users to be renamed, some methods have been changed to take a user's key rather than the user's name or an ApplicationUser rather than a directory User.  This has not happened to all such methods, and the code frequently has to convert back and forth between the different representations of a user.  To make these conversions easier to deal with, we have provided a class called ApplicationUsers, which is also heavily used in JIRA itself.  Since ApplicationUsers is a static-only utility class, it cannot use dependency injection to obtain the UserKeyService; it has to use the ComponentAccessor, instead.

How to Initialise the Component Accessor for Unit Tests

Unit tests must be responsible for ensuring that everything they require, directly or indirectly, is arranged during the test's setup.  The introduction of the UserKeyService and the use of the ComponentAccessor to resolve it in the implementation of ApplicationUsers.from(User) means that the ComponentAccessor is used much more heavily than it was in previous versions of JIRA.  This means in turn that many tests which previously did not need to worry about the ComponentAccessor will now have to initialise it.

JIRA's own unit tests deal with this by using com.atlassian.jira.mock.component.MockComponentWorker from the jira-tests artifact, which can be referenced using the maven dependency:

1
2
<dependency>
    <groupId>com.atlassian.jira</groupId>
    <artifactId>jira-tests</artifactId>
    <version>${jira.compile.version}</version>
    <scope>test</scope>
</dependency>

The easiest way for a unit test to initialise the ComponentAccessor is

1
2
@Before
public void setUp()
{
    new MockComponentWorker().init();
}

Note that the MockComponentWorker.init() method was added in JIRA 6.0.  In earlier versions, you would have to call the @Internal method ComponentAccessor.initialiseWorker(ComponentAccessor.Worker) explicitly to accomplish the same thing.  Either way, this sets the global reference that the ComponentAccessor needs in order to resolve components.  The MockComponentWorker comes with a few default mocks, including one for the UserKeyService that simply converts usernames to lowercase to produce a key, and this should be enough for the majority of tests.  If you need additional mocked components to be resolved in this way, then you can add them to it as well.  An example might look something like this:

1
2
@Before
public void setUp()
{
    final ApplicationUser fred = new MockApplicationUser("Fred");
    final JiraAuthenticationContext jiraAuthenticationContext = Mockito.mock(JiraAuthenticationContext.class);
    Mockito.when(jiraAuthenticationContext.getUser()).thenReturn(fred);
    Mockito.when(jiraAuthenticationContext.getLoggedInUser()).thenReturn(fred.getDirectoryUser());
    new MockComponentWorker()
        .addMock(ConstantsManager.class, new MockStatusConstantsManager())
        .addMock(JiraAuthenticationContext.class, jiraAuthenticationContext)
        .init();
}

Developers that are familiar with the JUnit 4 @Rule annotation may prefer to use the @AvailableInContainer annotation together with the MockComponentContainer rule or the MockitoMocksInContainer rule to accomplish the same thing.  These can also be found in the jira-tests artifact.

Cross-Product Plugin Development using Shared Access Layer (SAL)

If you develop a cross-product plugin, chances are you are using the Shared Access Layer to interact with the different products in a generic way. You should be aware that starting with version 2.10, SAL will be updated to better handle the fact that a user's username can change.
(Developers wanting early access to the API changes can get hold of SAL 2.10.0-m7 or later; JIRA 6.0-m10 and higher will implement these new methods).

UserManager and UserProfile

For access to a user's key, the UserProfile interface has been modifed and now includes a getUserKey() method. As explained above, if you need to store a reference to a user, you should do so by storing the user key, not the username.

Most of the methods in UserManager that used to take a username as a parameter are now deprecated in favour of methods that take a UserKey object instead. Similarly, the getRemoteUsername() methods in UserManager, which return a username as a String, are now deprecated in favour of ones that return a UserProfile or a UserKey object instead.

Other Services

Like for UserManager, all the methods in UserSettingsService and SearchProvider have been modified in a similar way: methods that take a username as a parameter are now deprecated, and new methods taking a UserKey instead have been added.

Rate this page: