Skip to main content

Migrating Existing Nexus Repository User Tokens from LDAP to SAML

This topic provides scripts and guidance to facilitate migrating existing Nexus Repository user tokens from Lightweight Directory Access Protocol (LDAP) to Security Assertion Markup Language (SAML).

For PostgreSQL/H2 Deployments on Nexus Version 3.59.0+

  1. Upgrade your Sonatype Nexus Repository instance to the latest version.

  2. Ensure that your SAML users have the same username as your LDAP users.

  3. In your field mapping, set up roles/groups that map to the same roles/groups as exist in LDAP.

    1. If role names are different, you must create different external role mappings in Nexus Repository.

    2. If using external role mapping for LDAP, you will need to recreate them for SAML.

  4. Enable scripting in $data-dir/etc/nexus.properties by adding nexus.scripts.allowCreation=true.

  5. Restart your Nexus Repository instance for the new setting to take effect.

  6. Back up your database immediately before running the script.

Caution

At this point, users can log in with SAML Auth or LDAP credentials. If you are using user tokens to authenticate, Nexus Repository will only use the LDAP realm tokens. However, if a SAML user uses the UI to access their token, this would create a new user token with new name and pass codes that will no longer match the LDAP side. A user could then use either LDAP or SAML user token to authenticate.

The script will move LDAP user tokens to SAML user tokens with identically named userid. If a SAML user accessed and created a new user token that is different from LDAP, the script will remove the LDAP token, and the existing SAML token will remain in place (ignored by the script).

Now, you can run the script provided below. You can do this either through the REST API or by adding a script task through the user interface. If using a script task, put the code provided below as the script, set the schedule to manual, then run it and watch the logs.

Once finished, disable scripting again by removing nexus.scripts.allowCreation=true or setting this to false in the $data-dir/etc/nexus.properties.

You can test that the migration has worked by disabling the LDAP realm and confirming that a user token still works by trying a known user token.

import com.sonatype.nexus.usertoken.plugin.apikey.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 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()
    newPrincipals.add(oldPrincipals.primaryPrincipal, '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)
    }
    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')

For All OrientDB Deployments or PostgreSQL/H2 Deployments on Versions Prior to 3.59.0

Before running the script, you must meet the following prerequisites:

  1. Ensure that your SAML users have the same username as your LDAP users.

  2. In your field mapping, set up roles/groups that map to the same roles/groups as exist in LDAP.

    1. If role names are different, you must create different external role mappings in Nexus Repository.

    2. If using external role mapping for LDAP, you will need to recreate them for SAML.

  3. Enable scripting in $data-dir/etc/nexus.properties by adding nexus.scripts.allowCreation=true.

  4. Restart your Nexus Repository instance for the new setting to take effect.

Now, you can run the script provided below. You can do this either through the REST API or by adding a script task through the user interface. If using a script task, put the code provided below as the script, set the schedule to manual, then run it and watch the logs.

Once finished, disable scripting again by removing nexus.scripts.allowCreation=true or setting this to false in the $data-dir/etc/nexus.properties.

You can test that the migration has worked by disabling the LDAP realm and confirming that a user token still works by trying a known user token.

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')