Skip to content

Deprecate UserIdMapper and just use an HMAC#10926

Merged
NotMyFault merged 7 commits intojenkinsci:masterfrom
jglick:User-HMAC
Aug 19, 2025
Merged

Deprecate UserIdMapper and just use an HMAC#10926
NotMyFault merged 7 commits intojenkinsci:masterfrom
jglick:User-HMAC

Conversation

@jglick
Copy link
Member

@jglick jglick commented Aug 6, 2025

Subsumes both #10870 and #10871, following @daniel-beck’s suggestion recorded in #10871 (comment). With this change, the logic of managing Users is simplified in most respects: every id gets a predictable directory name determined by a secure hash of the current IdStrategy and a per-controller secret, so no index is needed. The directory names are also strictly sanitized against dangerous characters (as UserIdMapper did), and it should not be possible to construct an id which would hash to a given directory name.

Testing done

A 2.516.x version passes CloudBees CI PCT & ATH. 2d18199 passed bom PCT. Light interactive testing with mock-security-realm.

Proposed changelog entries

  • Pick a new name format for subdirectories of $JENKINS_HOME/users/.
  • Stop creating redundant $JENKINS_HOME/users/users.xml.

Proposed changelog category

/label removed

Proposed upgrade guidelines

No action is necessary when upgrading. Downgrading past this version (without restoring from backup) may lose user records including configuration such as API tokens.

Maintainer checklist

  • There are at least two (2) approvals for the pull request and no outstanding requests for change.
  • Conversations in the pull request are over, or it is explicit that a reviewer is not blocking the change.
  • Changelog entries in the pull request title and/or Proposed changelog entries are accurate, human-readable, and in the imperative mood.
  • Proper changelog labels are set so that the changelog can be generated automatically.
  • If the change needs additional upgrade steps from users, the upgrade-guide-needed label is set and there is a Proposed upgrade guidelines section in the pull request title (see example).
  • If it would make sense to backport the change to LTS, a Jira issue must exist, be a Bug or Improvement, and be labeled as lts-candidate to be considered (see query).

@comment-ops-bot comment-ops-bot bot added the removed This PR removes a feature or a public API label Aug 6, 2025
Comment on lines 717 to 718
or greater issues in the realm change, could affect currently logged
in users and even the user making the change. */
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure rekey has any real test coverage.

@Test
void caseInsensitivity() {
j.jenkins.setSecurityRealm(new IdStrategySpecifyingSecurityRealm(new IdStrategy.CaseInsensitive()));
User user = User.get("john smith");
User user2 = User.get("John Smith");
assertSame(user.getId(), user2.getId(), "Users should have the same id.");
}
@Test
void caseSensitivity() {
j.jenkins.setSecurityRealm(new IdStrategySpecifyingSecurityRealm(new IdStrategy.CaseSensitive()));
User user = User.get("john smith");
User user2 = User.get("John Smith");
assertNotSame(user.getId(), user2.getId(), "Users should not have the same id.");
assertEquals("john smith", User.idStrategy().keyFor(user.getId()));
assertEquals("John Smith", User.idStrategy().keyFor(user2.getId()));
}
@Test
void caseSensitivityEmail() {
j.jenkins.setSecurityRealm(new IdStrategySpecifyingSecurityRealm(new IdStrategy.CaseSensitiveEmailAddress()));
User user = User.get("john.smith@acme.org");
User user2 = User.get("John.Smith@acme.org");
assertNotSame(user.getId(), user2.getId(), "Users should not have the same id.");
assertEquals("john.smith@acme.org", User.idStrategy().keyFor(user.getId()));
assertEquals("John.Smith@acme.org", User.idStrategy().keyFor(user2.getId()));
user2 = User.get("john.smith@ACME.ORG");
assertEquals(user.getId(), user2.getId(), "Users should have the same id.");
assertEquals("john.smith@acme.org", User.idStrategy().keyFor(user2.getId()));
}
private static class IdStrategySpecifyingSecurityRealm extends HudsonPrivateSecurityRealm {
private final IdStrategy idStrategy;
IdStrategySpecifyingSecurityRealm(IdStrategy idStrategy) {
super(true, false, null);
this.idStrategy = idStrategy;
}
@Override
public IdStrategy getUserIdStrategy() {
return idStrategy;
}
}
do not change the strategy after user records have been created.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Interactive testing with mock-security-realm seems fine: switching the id strategy renames the dir of a user whose id contains uppercase letters.)

return new File(Jenkins.get().getRootDir(), "users");
}

private static final int PREFIX_MAX = 14;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original version in UserIdMapper set this to 15 but then subtracted one for the _. I find this a bit clearer. Of course the size is arbitrary; here it allows directory names up to 79 (ASCII) characters, which I think is safe on all operating systems. The bigger concern on Windows is a total path length approaching (IIRC) 254, when not using the UNC \\?\ trick, but if you control the %JENKINS_HOME% length, this should not be too bad: C:\Jenkins\users\quixotichacker_01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b\config.xml is only 107.

Copy link
Member

@jtnord jtnord Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bigger concern on Windows is a total path length approaching (IIRC) 254, when not using the UNC \?\ trick, but if you control the %JENKINS_HOME% length, this should not be too bad:

Java has been using the unicode API with \\?\ for a loooong time.
https://github.com/openjdk/jdk21u-dev/blame/d90297a828dff468afc34e2767439e51379f4f95/src/java.base/windows/classes/sun/nio/fs/WindowsPath.java#L174-L178

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I just recall problems with branch-api on Windows.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with branch-api on Windows.

🤔 it should have only been an issue with other tools (like git.exe) and the like, nothing directly inside the JVM (calling cmd.exe from a durable task for example in a long directory would be fun).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an issue with other tools (like git.exe) and the like

Ah, perhaps so, in which case that would indeed not be a concern here.

return userDetails.getUsername();
} catch (UsernameNotFoundException x) {
LOGGER.log(Level.FINE, "not sure whether " + idOrFullName + " is a valid username or not", x);
LOGGER.log(Level.FINER, "not sure whether " + idOrFullName + " is a valid username or not", x);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noise in UserTest.

@jglick jglick marked this pull request as ready for review August 11, 2025 13:07
This was referenced Aug 11, 2025
@jglick jglick requested review from a team, Vlatombe and daniel-beck August 11, 2025 13:08
XSTREAM.alias("user", User.class);
}

private User() {}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a subtle difference between calling the no-arg ctor and unmarshalling into that object, vs. deserializing a new object: whether properties (and, if anyone cares, version) are initialized.

Comment on lines +830 to +831
var d = getUserFolderFor(id);
return d.isDirectory() ? d : null;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dubious (normally you would just want to get the File unconditionally whether it currently exists or not), but following the existing nullability annotation and semantics.

Comment on lines +1106 to +1108
if (!migratedUserIdMapper) {
try {
UserIdMapper.migrate();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May happen prior to scanAll if JCasC defines users in the built-in realm (mostly useful for demos).

Copy link
Member

@jtnord jtnord left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable but appears to be lacking any test coverage for the older data migration?

user.id = idKey; // not quite right but hoping for the best
userXml.write(user);
}
var newDirectory = User.getUserFolderFor(user.id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems weired that the users folder uses the normalized user id, but we don;t actually store that in the User.id here or when creating a new User?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I am not following this comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related (directly) to this change.
if you say log in first with "JOE" then your user.id can be JOE (but the keyfor will create this file as it if is for "joe". if you later log in with "joe" (your user will load, but your user.id will be "JOE") (yes its the same user, but there is no normalisation going on).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed the first login will store $JENKINS_HOME/users/joe_deadbeef1234/config.xml with

<id>JOE</id>

and I suppose that will be your id henceforth, even if User.getById("joe", …) is called. I think nothing in this PR is changing that; the previous format would have written the same XML content to $JENKINS_HOME/users/JOE_11772365/config.xml and an entry joeJOE_11772365 to $JENKINS_HOME/users.xml but otherwise behaved the same.

private final int version = 1; // Not currently used, but it may be helpful in the future to store a version.

private transient File usersDirectory;
// contrary to the name, the keys were actually IdStrategy.keyFor, not necessarily ids
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what this comment is trying to imply, keyFor creates the normalized id. If you are saying that this can also contain Names (when the id is not known and this is from say an SCM commit) then that should not be persisted so should not have a directory Name?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keyFor creates the normalized id

Right, which is not necessarily the id. So the map should really have been named something like idKeyToDirectoryName. That is all.

@jglick
Copy link
Member Author

jglick commented Aug 11, 2025

lacking any test coverage for the older data migration

No newly written coverage currently, though various cases are in fact tested implicitly via @LocalData and @PresetData.

Copy link
Member

@Vlatombe Vlatombe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@timja
Copy link
Member

timja commented Aug 18, 2025

/label ready-for-merge


This PR is now ready for merge, after ~24 hours, we will merge it if there's no negative feedback.

Thanks!

@comment-ops-bot comment-ops-bot bot added the ready-for-merge The PR is ready to go, and it will be merged soon if there is no negative feedback label Aug 18, 2025
Copy link

@A1exKH A1exKH left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Copy link
Member

@NotMyFault NotMyFault left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seams reasonable

@NotMyFault NotMyFault merged commit fef9250 into jenkinsci:master Aug 19, 2025
18 checks passed
@jglick jglick deleted the User-HMAC branch August 19, 2025 11:39
@PallierJean
Copy link

Hello,

I am encountering a problem when using the latest version of Jenkins in my company.

Jenkins is deployed in a k3s cluster as a pod. Each Jenkins pod is customised using the as code plugin.
This allows us to have Jenkins instances configured differently for each team.

We back up the user folder and the jov folder so that we do not lose our data.
In the user folder, we have the users and tokens.
When we redeploy the pod to get a new version of the image or additional customisation, we noticed that a new folder suffixed with an HMAC was created. However, it does not migrate the token correctly.
I see that in the users.xml file, the link between the user and the newly created folder is made.
The problem is that the ID of the jenkins user, which allows us to use user:token to access the jenkins APIs, keeps changing with each redeployment.
With the latest version, it is no longer possible to use the tokens. There is a bug: they are not transferred to the new folder.
We therefore lose the bot or user tokens with each redeployment.

Thanks,

J. Pallier (Team CI from Lectra)

@daniel-beck
Copy link
Member

Token migration problems from this PR are tracked in https://issues.jenkins.io/browse/JENKINS-76269.

@long76
Copy link

long76 commented Dec 23, 2025

please check #16840 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-merge The PR is ready to go, and it will be merged soon if there is no negative feedback removed This PR removes a feature or a public API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants