SonarSource/sonarqube · error · UnauthorizedException

User %s is not part of allowed workspaces list

Error message

User %s is not part of allowed workspaces list

What it means

Thrown by checkTeamRestriction when workspace allowlisting is configured and the workspace-access response from Bitbucket is null or contains no workspaces, so the user cannot be matched against any allowed workspace. It is an UnauthorizedException: authentication succeeded but the user is denied by policy.

Source

Thrown at server/sonar-auth-bitbucket/src/main/java/org/sonar/auth/bitbucket/BitbucketIdentityProvider.java:172

  @CheckForNull
  private GsonEmails requestEmails(OAuth20Service service, OAuth2AccessToken accessToken) throws InterruptedException, ExecutionException, IOException {
    OAuthRequest userRequest = new OAuthRequest(Verb.GET, settings.apiURL() + "2.0/user/emails");
    service.signRequest(accessToken, userRequest);
    Response emailsResponse = service.execute(userRequest);
    if (emailsResponse.isSuccessful()) {
      return GsonEmails.parse(emailsResponse.getBody());
    }
    return null;
  }

  private void checkTeamRestriction(OAuth20Service service, OAuth2AccessToken accessToken, GsonUser user) throws InterruptedException, ExecutionException, IOException {
    String[] workspaceAllowed = settings.workspaceAllowedList();
    if (workspaceAllowed != null && workspaceAllowed.length > 0) {
      GsonWorkspaceAccesses userWorkspaces = requestWorkspaces(service, accessToken);
      String errorMessage = format("User %s is not part of allowed workspaces list", user.getUsername());
      if (userWorkspaces == null || userWorkspaces.getWorkspaces() == null) {
        throw new UnauthorizedException(errorMessage);
      } else {
        Set<String> uniqueUserWorkspaceSlugs = userWorkspaces.getWorkspaces().stream().map(w -> w.getWorkspace().getSlug()).collect(toSet());
        List<String> workspaceAllowedList = asList(workspaceAllowed);
        if (uniqueUserWorkspaceSlugs.stream().anyMatch(workspaceAllowedList::contains)) {
          return;
        }
        List<String> workspaceNames = requestWorkspaceNames(service, accessToken, uniqueUserWorkspaceSlugs);
        if (workspaceNames.stream().noneMatch(workspaceAllowedList::contains)) {
          throw new UnauthorizedException(errorMessage);
        }
      }
    }
  }

  @CheckForNull
  private GsonWorkspaceAccesses requestWorkspaces(OAuth20Service service, OAuth2AccessToken accessToken) throws InterruptedException, ExecutionException, IOException {
    OAuthRequest userRequest = new OAuthRequest(Verb.GET, settings.apiURL() + "2.0/user/workspaces");
    service.signRequest(accessToken, userRequest);

View on GitHub (pinned to 184c821202)

Solutions

  1. Grant the OAuth consumer the workspace membership scope so requestWorkspaces can read the user's workspaces.
  2. Confirm the user actually belongs to one of the workspaces listed in sonar.auth.bitbucket.workspaces.
  3. Check the workspaces API response (add logging) to distinguish 'no memberships' from 'unreadable due to scope'.
  4. If the restriction is unintended, clear the workspace allowlist setting.
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check that the user has workspace data before enforcing restriction
if (userWorkspaces == null || userWorkspaces.getWorkspaces() == null || userWorkspaces.getWorkspaces().isEmpty()) {
    LOG.warn("No workspace memberships returned for user {} — check token scopes", user.getUsername());
}

Try / catch

try {
    checkTeamRestriction(service, accessToken, user);
} catch (UnauthorizedException e) {
    LOG.warn("Workspace restriction rejected login: {}", e.getMessage());
}

Prevention

When it happens

Trigger: settings.workspaceAllowedList() is non-empty, requestWorkspaces returns a GsonWorkspaceAccesses whose getWorkspaces() is null (or the whole object is null), immediately throwing before slug comparison.

Common situations: Bitbucket returns an empty/absent workspaces page (user truly has no workspace memberships); the token lacks the workspace scope so Bitbucket returns no workspace data; apiURL misconfigured so the workspaces endpoint silently returns an empty payload.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of SonarSource/sonarqube@184c821202 (2026-09-09). Data as JSON: /api/errors/886bbb895f73fe4f. Report an issue: GitHub.