Migrating User Tokens from LDAP to SAML
This topic provides a method to migrate Nexus Repository user tokens from Lightweight Directory Access Protocol (LDAP) to Security Assertion Markup Language (SAML). These methods require that the SAML users have the same username as your LDAP users and that the SAML roles and groups map to the same roles and groups as exist in LDAP.
This functionality moves LDAP user tokens to SAML user tokens with identically named userid. When a SAML user accessed and created a new user token that is different from LDAP, this feature removes the LDAP token, and the existing SAML token remains in place.
There are two methods to use this feature:
Using a built-in task for Nexus Repository versions 3.80+ (Preferred)
Running the included groovy scripts using the Script API for your repository version
LDAP to SAML user tokens migration task
To use the built-in task to migrate existing user tokens for LDAP to SAML realm, take the following steps:
Back up your Nexus Repository database; we highly recommend taking this step as a precaution in case you need to roll back.
The built-in task is not visible by default. Make the task available in your instance by adding
nexus.apikey.usertoken.ldapsamlrealm.migraterealm.expose=trueto yournexus.propertiesfile. By default, this file is in your Data Directory under$data-dir/etc/nexus.properties.Create the ApiKey/UserToken – LDAP to SAML user tokens migration task through the user interface under Settings > System > Tasks . See Tasks for details on creating tasks in Nexus Repository.
Manually run the task to perform the migration.
Using the Scripts
Upgrade Nexus Repository instance to the latest version.
Set the SAML configuration in Nexus Repository, and enable the SAML realm
See SAML
Enable scripting for Nexus Repository
See Script API
Restart Nexus Repository
Backup the database before running the script
Run the script provided below using the script API or by adding it to a "Admin - Execute script" task
See Tasks
Once finished, disable scripting again
Test that the migration has worked by disabling the LDAP realm and confirming that a user token works by trying a known user token
/**
* Copyright (c) 2023-present Sonatype, Inc. All rights reserved.
* Sonatype" is a trademark of Sonatype, Inc.
* Provided "as is" with no implied official support.
*/
/**
* IMPORTANT: requires nexus 3.70.0 or greater
*
* Scenario: Nexus 3 running with following configurations
* 1) LDAP Realm enabled
* 2) SAML Realm enabled
* 3) User Tokens enabled
* 4) Configured UserTokens are associated with the LDAP realm
*
* Objective: Update LDAP user tokens to now be associated with the SAML realm
*
* Notable Restrictions:
* 1) The LDAP userId is expected to match the SAML userId exactly (case sensitivity counts)
*
* Process:
* 1) Inspect every userToken, looking for those that are associated with the LdapRealm
* 2) Update these tokens, replacing the LdapRealm association with the SamlRealm
* 3) For each token updated, also create a local SamlUser from the associated LdapUser so that the tokens can be
* utilized without first requiring every user to authenticate with SAML
*
*/
import com.sonatype.nexus.usertoken.plugin.apikey.store.UserTokenStore
import org.sonatype.nexus.security.user.User
import org.sonatype.nexus.security.user.UserManager
import org.apache.shiro.subject.SimplePrincipalCollection
UserTokenStore userTokenStore = container.lookup(UserTokenStore) as UserTokenStore
UserManager ldapUserManager = container.lookup(UserManager, "LDAP")
UserManager samlUserManager = container.lookup(UserManager, "SAML")
log.info('starting user token migration from LDAP to SAML')
userTokenStore.records().each { userTokenRecord ->
log.info("Found token principal ${userTokenRecord.principals}")
SimplePrincipalCollection oldPrincipals = new SimplePrincipalCollection(userTokenRecord.principals)
if (!oldPrincipals.fromRealm('SamlRealm').empty) {
log.info('skipping token as it already has SAML realm')
}
else if (!oldPrincipals.fromRealm('LdapRealm').empty) {
SimplePrincipalCollection newPrincipals = new SimplePrincipalCollection()
def userId = oldPrincipals.primaryPrincipal
newPrincipals.add(userId, 'SamlRealm')
userTokenStore.remove(userTokenRecord.principals)
try {
def newUserTokenRecord = userTokenStore.newUserTokenRecord(newPrincipals, userTokenRecord.userToken, userTokenRecord.created.toInstant().atOffset(java.time.ZoneOffset.UTC))
log.info('Adding SAML realm principal to token')
log.debug('{}', newUserTokenRecord)
userTokenStore.add(newUserTokenRecord)
addCachedSamlUser(userId, ldapUserManager, samlUserManager)
}
catch (com.sonatype.nexus.usertoken.plugin.store.DuplicateUserTokenException e) {
log.info('skipping token as it already has SAML realm')
}
catch (Exception e) {
try {
log.warn('Attempting to add back the original UserTokenRecord')
def originalUserTokenRecord = userTokenStore.newUserTokenRecord(userTokenRecord.principals, userTokenRecord.userToken, userTokenRecord.created.toInstant().atOffset(java.time.ZoneOffset.UTC))
log.debug('{}', originalUserTokenRecord)
userTokenStore.add(originalUserTokenRecord)
}
catch (Exception e1) {
log.error('Unable to add back the original UserTokenRecord', e1)
}
throw e
}
}
else {
log.info('Skipping non-LDAP token')
}
}
log.info('completed user token migration to SAML')
def addCachedSamlUser(userId, ldapUserManager, samlUserManager) {
try {
User ldapUser = ldapUserManager.getUser(userId)
if (!ldapUser) {
log.error("Unable to retrieve user ${userId} from ldap, skipping creation of local SAML user")
return
}
samlUserManager.addUser(ldapUser, null)
}
catch (Exception e) {
log.error("Failed to retrieve user ${userId} from ldap, skipping creation of local SAML user", e)
}
}
import com.sonatype.nexus.usertoken.plugin.store.UserTokenStore
import org.apache.shiro.subject.SimplePrincipalCollection
UserTokenStore userTokenStore = container.lookup(UserTokenStore) as UserTokenStore
log.info('starting user token migration from LDAP to SAML')
userTokenStore.records().each { userTokenRecord ->
log.info("Found token principal ${userTokenRecord.principals}")
SimplePrincipalCollection newPrincipals = new SimplePrincipalCollection(userTokenRecord.principals)
if (!newPrincipals.fromRealm('SamlRealm').empty) {
log.info('skipping token as it already has SAML realm')
}
else if (!newPrincipals.fromRealm('LdapRealm').empty) {
newPrincipals.add(newPrincipals.primaryPrincipal, 'SamlRealm')
userTokenStore.remove(userTokenRecord.nameCode)
try {
def newUserTokenRecord = userTokenRecord.getClass().newInstance(newPrincipals, userTokenRecord.userToken, userTokenRecord.created)
log.info('Adding SAML realm principal to token')
log.debug("{}", newUserTokenRecord)
userTokenStore.add(newUserTokenRecord)
} catch (Exception e) {
try {
log.warn("Attempting to add back the original UserTokenRecord")
def originalUserTokenRecord = userTokenRecord.getClass().newInstance(userTokenRecord.principals, userTokenRecord.userToken, userTokenRecord.created)
log.debug("{}", originalUserTokenRecord)
userTokenStore.add(originalUserTokenRecord)
} catch (Exception e1) {
log.error("Unable to add back the original UserTokenRecord", e1)
}
throw e;
}
}
else {
log.info('Skipping non-LDAP token')
}
}
log.info('completed user token migration to SAML')