Migrating SonarQube Authentication for 3,000+ Users: Lessons Learned in Transitioning to SAML

Kadir Islow | Nov 6, 2024 min read

Migrating 3,000+ Users

This post aims to provide guidance and lessons learned for those undertaking similar authentication migrations in their SonarQube environments, whether moving from Azure AD, LDAP, or other existing authentication integrations to a SAML-based approach.

When migrating authentication methods in SonarQube, seemingly small details can make a significant difference in user access. I’ll dive into the important externalProvider property and how it impacted our migration of over 3,000 users to the new SAML authentication setup.

Before we proceed, what is SonarQube again?

TLDR; It’s a code quality and security platform that performs automatic reviews of code to detect bugs, code smells, and security vulnerabilities.

As a cornerstone of modern DevSecOps practices, it empowers teams to integrate security directly into the development process. SonarQube supports 27+ programming languages and integrates seamlessly with popular CI/CD pipelines to ensure code quality throughout the development lifecycle.

Understanding the Authentication Landscape

SonarQube supports multiple authentication methods, including Azure Active Directory/Entra ID (formerly known as AAD) and SAML. Each method relies on a specific mechanism to identify and authenticate users. The key to this process lies in the externalProvider property, a behind-the-scenes attribute that determines user login capabilities.

The Authentication Property: externalProvider

In our specific scenario, we were transitioning from an AAD plugin to SAML authentication. The externalProvider property plays a crucial role in this migration:

  • For AAD authentication: externalProvider = aad
  • For SAML authentication: externalProvider = saml

Migration

Migration Challenges

Here’s the critical insight: Users will be locked out if the externalProvider is not updated during the SAML migration.

This means that even with correct SAML configuration, users won’t be able to log in unless their externalProvider value is explicitly changed to saml.

Migration Steps

  1. Always test authentication changes in a staging environment
  2. Document each step of the migration process
  3. Communicate changes clearly to your team and impacted departments

Create a backup

def create_backup_file(data: list, file: str) -> None:
    data = data
    format_data = json.dumps(data, indent=2)

    # Create the backup directory if it doesn't exist
    backup_dir = "./backup"
    os.makedirs(backup_dir, exist_ok=True)

    # Append timestamp to the file name
    timestamp = time.strftime("%Y%m%d%H%M%S")
    file_name = f"{file}_{timestamp}.json"
    file_path = os.path.join(backup_dir, file_name)

    with open(file_path, 'w') as backup:
        backup.write(str(format_data))

    print("Backup saved in:", file_path)

Prepare rollback functionality

def restore_authentication_method(account: str, stage: str):
    secret: dict = json.loads(get_secret_value(account))
    base_url: str = stage

    # sonarqube token is sent via the login field of HTTP basic authentication, without password. (curl -u token: $URL)
    token: str = secret["token"]
    no_pwd: str = ""
    file = open('restore_file.json')

    migration_data = json.load(file)
    for user in migration_data:
        login: str = user["login"]
        externalIdentity: str = user["externalIdentity"]
        initialExternalProvider: str = user["externalProvider"]

        url: str = f"{base_url}/api/users/update_identity_provider?login={login}&newExternalProvider={initialExternalProvider}&newExternalIdentity={externalIdentity}"
        response: Response = post(url, auth=HTTPBasicAuth(token, no_pwd))

        print("-----------------------------------------------------------")
        print(f"Updating auth for {login}")
        print(url)
        response.raise_for_status()

Verify Current Configuration

  1. Check existing user configurations
  2. Identify users with aad as their externalProvider
def get_user_migration_data(account: str, stage: str) -> list:
    data = get_user_data(account, stage)

    migration_data = []

    for item in data:
        new_dict = {
            'login': item['login'],
            'externalIdentity': item['externalIdentity'],
            'externalProvider': item['externalProvider']
        }
        migration_data.append(new_dict)

    return migration_data


def get_user_data(account: str, stage: str) -> None:
    secret: dict = json.loads(get_secret_value(account))
    base_url: str = stage

    # sonarqube token is sent via the login field of HTTP basic authentication, without password. (curl -u token: $URL)
    token: str = secret["token"]
    no_pwd: str = ""
    params = {
        "p": 1,
        "ps": 500
    }

    user_data: List[dict] = []
    url: str = f"{base_url}/api/users/search"

    # handle pagination
    while True:
        response: Response = get(url, auth=HTTPBasicAuth(token, no_pwd), params=params)
        raise_for_status(response=response)
        data: dict = response.json()
        user_data.extend(data["users"])
        if len(data["users"]) < params["ps"]:
            break
        params["p"] += 1

    return user_data

Update User Properties

Modify the externalProvider to saml

def update_authentication_method(account: str, stage: str):
    secret: dict = json.loads(get_secret_value(account))
    base_url: str = stage

    # sonarqube token is sent via the login field of HTTP basic authentication, without password. (curl -u token: $URL)
    token: str = secret["token"]
    no_pwd: str = ""

    migration_data = get_user_migration_data(account, stage)
    for user in migration_data:
        login: str = user["login"]
        externalIdentity: str = user["externalIdentity"]
        externalProvider: str = user["externalProvider"]
        newExternalProvider: str = "saml"

        # only update users with aad auth method
        if externalProvider != "aad":
            continue

        url: str = f"{base_url}/api/users/update_identity_provider?login={login}&newExternalProvider={newExternalProvider}&newExternalIdentity={externalIdentity}"
        response: Response = post(url, auth=HTTPBasicAuth(token, no_pwd))

        print("-----------------------------------------------------------")
        print(f"Updating auth for {login}")
        print(url)
        response.raise_for_status()

Test Incrementally

  1. Update a small group of users first in your dev environment
  2. Validate SAML login functionality
  3. Update a small group of users in production
  4. Gradually roll out to entire user base

Conclusion

Authentication migrations require careful attention. The externalProvider property might seemed like a minor configuration element, but it could be the difference between smooth access and user lockout.

Remember: In authentication migrations, the devil is in the details!