Переглянути джерело

SONAR-13930 Allow migration of auth system

tags/8.7.0.41497
Jacek 3 роки тому
джерело
коміт
a915585e91

+ 3
- 0
server/sonar-docs/src/pages/instance-administration/delegated-auth.md Переглянути файл

@@ -230,6 +230,9 @@ Authentication will be tried on each server, in the order they are listed in the

Note that all the LDAP servers must be available while (re)starting the SonarQube server.

### Migrate users to a new authentication method
If you are changing your delegated authentication method and migrating existing users from your previous authentication method, you can use the `api/users/update_identity_provider` web API to update your users' identity provider.

### Troubleshooting
* Detailed connection logs (and potential error codes received from LDAP server) are output to SonarQube's _$SONARQUBE_HOME/logs/web.log_, when logging is in `DEBUG` mode.


+ 193
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UpdateIdentityProviderAction.java Переглянути файл

@@ -0,0 +1,193 @@
/*
* SonarQube
* Copyright (C) 2009-2020 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.user.ws;

import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.sonar.api.server.authentication.IdentityProvider;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.server.ws.WebService.NewController;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.user.UserDto;
import org.sonar.server.authentication.IdentityProviderRepository;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.user.ExternalIdentity;
import org.sonar.server.user.UpdateUser;
import org.sonar.server.user.UserSession;
import org.sonar.server.user.UserUpdater;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
import static org.sonarqube.ws.client.user.UsersWsParameters.ACTION_UPDATE_IDENTITY_PROVIDER;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_LOGIN;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_NEW_EXTERNAL_IDENTITY;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_NEW_EXTERNAL_PROVIDER;

public class UpdateIdentityProviderAction implements UsersWsAction {

private final DbClient dbClient;
private final IdentityProviderRepository identityProviderRepository;
private final UserUpdater userUpdater;
private final UserSession userSession;

public UpdateIdentityProviderAction(DbClient dbClient, IdentityProviderRepository identityProviderRepository,
UserUpdater userUpdater, UserSession userSession) {
this.dbClient = dbClient;
this.identityProviderRepository = identityProviderRepository;
this.userUpdater = userUpdater;
this.userSession = userSession;
}

@Override
public void define(NewController controller) {
WebService.NewAction action = controller.createAction(ACTION_UPDATE_IDENTITY_PROVIDER)
.setDescription("Update identity provider information. <br/>"
+ "It's only possible to migrate to an installed identity provider. "
+ "Be careful that as soon as this information has been updated for a user, "
+ "the user will only be able to authenticate on the new identity provider. "
+ "It is not possible to migrate external user to local one.<br/>"
+ "Requires Administer System permission.")
.setSince("8.7")
.setInternal(false)
.setPost(true)
.setHandler(this);

action.createParam(PARAM_LOGIN)
.setDescription("User login")
.setRequired(true);
action.createParam(PARAM_NEW_EXTERNAL_PROVIDER)
.setRequired(true)
.setDescription("New external provider. Only authentication system installed are available. Use 'sonarqube' identity provider for LDAP.");

action.createParam(PARAM_NEW_EXTERNAL_IDENTITY)
.setDescription("New external identity, usually the login used in the authentication system. "
+ "If not provided previous identity will be used.");
}

@Override
public void handle(Request request, Response response) throws Exception {
userSession.checkLoggedIn().checkIsSystemAdministrator();
UpdateIdentityProviderRequest wsRequest = toWsRequest(request);
doHandle(wsRequest);
response.noContent();
}

private void doHandle(UpdateIdentityProviderRequest request) {
checkEnabledIdentityProviders(request.newExternalProvider);
try (DbSession dbSession = dbClient.openSession(false)) {
UserDto user = getUser(dbSession, request.login);
ExternalIdentity externalIdentity = getExternalIdentity(request, user);
userUpdater.updateAndCommit(dbSession, user, new UpdateUser().setExternalIdentity(externalIdentity), u -> {
});
}
}

private void checkEnabledIdentityProviders(String newExternalProvider) {
List<String> availableIdentityProviders = getAvailableIdentityProviders();
checkArgument(availableIdentityProviders.contains(newExternalProvider), "Value of parameter 'newExternalProvider' (%s) must be one of: [%s]", newExternalProvider,
String.join(", ", availableIdentityProviders));
}

private List<String> getAvailableIdentityProviders() {
List<String> discoveredProviders = identityProviderRepository.getAllEnabledAndSorted()
.stream()
.map(IdentityProvider::getKey)
.collect(Collectors.toList());
discoveredProviders.add(SQ_AUTHORITY);
return discoveredProviders;
}

private UserDto getUser(DbSession dbSession, String login) {
UserDto user = dbClient.userDao().selectByLogin(dbSession, login);
if (user == null || !user.isActive()) {
throw new NotFoundException(format("User '%s' doesn't exist", login));
}
return user;
}

private static ExternalIdentity getExternalIdentity(UpdateIdentityProviderRequest request, UserDto user) {
return new ExternalIdentity(
request.newExternalProvider,
request.newExternalIdentity != null ? request.newExternalIdentity : user.getExternalLogin(),
null);
}

private static UpdateIdentityProviderRequest toWsRequest(Request request) {
return UpdateIdentityProviderRequest.builder()
.setLogin(request.mandatoryParam(PARAM_LOGIN))
.setNewExternalProvider(request.mandatoryParam(PARAM_NEW_EXTERNAL_PROVIDER))
.setNewExternalIdentity(request.param(PARAM_NEW_EXTERNAL_IDENTITY))
.build();
}

static class UpdateIdentityProviderRequest {
private final String login;
private final String newExternalProvider;
private final String newExternalIdentity;

public UpdateIdentityProviderRequest(Builder builder) {
this.login = builder.login;
this.newExternalProvider = builder.newExternalProvider;
this.newExternalIdentity = builder.newExternalIdentity;
}

public static UpdateIdentityProviderRequest.Builder builder() {
return new UpdateIdentityProviderRequest.Builder();
}

static class Builder {
private String login;
private String newExternalProvider;
private String newExternalIdentity;

private Builder() {
// enforce factory method use
}

public Builder setLogin(String login) {
this.login = login;
return this;
}

public Builder setNewExternalProvider(String newExternalProvider) {
this.newExternalProvider = newExternalProvider;
return this;
}

public Builder setNewExternalIdentity(@Nullable String newExternalIdentity) {
this.newExternalIdentity = newExternalIdentity;
return this;
}

public UpdateIdentityProviderRequest build() {
checkArgument(!isNullOrEmpty(login), "Login is mandatory and must not be empty");
checkArgument(!isNullOrEmpty(newExternalProvider), "New External Provider is mandatory and must not be empty");
return new UpdateIdentityProviderRequest(this);
}
}
}

}

+ 2
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UsersWsModule.java Переглянути файл

@@ -46,7 +46,8 @@ public class UsersWsModule extends Module {
UserJsonWriter.class,
SetHomepageAction.class,
HomepageTypesImpl.class,
SetSettingAction.class);
SetSettingAction.class,
UpdateIdentityProviderAction.class);

if (configuration.getBoolean(ProcessProperties.Property.SONARCLOUD_ENABLED.getKey()).orElse(false)) {
// onboarding tutorial is available only in SonarCloud

+ 189
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/UpdateIdentityProviderActionTest.java Переглянути файл

@@ -0,0 +1,189 @@
/*
* SonarQube
* Copyright (C) 2009-2020 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.user.ws;

import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.user.UserDto;
import org.sonar.server.authentication.CredentialsLocalAuthentication;
import org.sonar.server.authentication.IdentityProviderRepositoryRule;
import org.sonar.server.authentication.TestIdentityProvider;
import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.exceptions.UnauthorizedException;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.user.NewUserNotifier;
import org.sonar.server.user.UserUpdater;
import org.sonar.server.user.index.UserIndexer;
import org.sonar.server.usergroups.DefaultGroupFinder;
import org.sonar.server.ws.TestRequest;
import org.sonar.server.ws.WsActionTester;

import static com.google.common.collect.Lists.newArrayList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.sonar.db.user.UserTesting.newUserDto;

public class UpdateIdentityProviderActionTest {
private final static String SQ_AUTHORITY = "sonarqube";

@Rule
public IdentityProviderRepositoryRule identityProviderRepository = new IdentityProviderRepositoryRule()
.addIdentityProvider(new TestIdentityProvider().setName("Gitlab").setKey("gitlab").setEnabled(true))
.addIdentityProvider(new TestIdentityProvider().setName("Github").setKey("github").setEnabled(true));

@Rule
public DbTester db = DbTester.create();
@Rule
public EsTester es = EsTester.create();
@Rule
public UserSessionRule userSession = UserSessionRule.standalone().logIn().setSystemAdministrator();

private final MapSettings settings = new MapSettings();
private final DbClient dbClient = db.getDbClient();
private final DbSession dbSession = db.getSession();
private final UserIndexer userIndexer = new UserIndexer(dbClient, es.client());
private final CredentialsLocalAuthentication localAuthentication = new CredentialsLocalAuthentication(dbClient);

private final WsActionTester underTest = new WsActionTester(new UpdateIdentityProviderAction(dbClient, identityProviderRepository,
new UserUpdater(mock(NewUserNotifier.class), dbClient, userIndexer, new DefaultGroupFinder(db.getDbClient()), settings.asConfig(), localAuthentication),
userSession));

@Test
public void change_identity_provider_of_a_local_user_all_params() {
String userLogin = "login-1";
String newExternalLogin = "login@github.com";
String newExternalIdentityProvider = "github";
createUser(true, userLogin, userLogin, SQ_AUTHORITY);
TestRequest request = underTest.newRequest()
.setParam("login", userLogin)
.setParam("newExternalProvider", newExternalIdentityProvider)
.setParam("newExternalIdentity", newExternalLogin);

request.execute();
assertThat(dbClient.userDao().selectByExternalLoginAndIdentityProvider(dbSession, newExternalLogin, newExternalIdentityProvider))
.isNotNull()
.extracting(UserDto::isLocal, UserDto::getExternalLogin, UserDto::getExternalIdentityProvider)
.contains(false, newExternalLogin, newExternalIdentityProvider);
}

@Test
public void change_identity_provider_of_a_local_user_mandatory_params_only_provider_login_stays_same() {
String userLogin = "login-1";
String newExternalIdentityProvider = "github";
createUser(true, userLogin, userLogin, SQ_AUTHORITY);
TestRequest request = underTest.newRequest()
.setParam("login", userLogin)
.setParam("newExternalProvider", newExternalIdentityProvider);

request.execute();
assertThat(dbClient.userDao().selectByExternalLoginAndIdentityProvider(dbSession, userLogin, newExternalIdentityProvider))
.isNotNull()
.extracting(UserDto::isLocal, UserDto::getExternalLogin, UserDto::getExternalIdentityProvider)
.contains(false, userLogin, newExternalIdentityProvider);
}

@Test
public void change_identity_provider_of_a_external_user_to_new_one() {
String userLogin = "login-1";
String oldExternalIdentityProvider = "gitlab";
String oldExternalIdentity = "john@gitlab.com";
createUser(false, userLogin, oldExternalIdentity, oldExternalIdentityProvider);

String newExternalIdentityProvider = "github";
String newExternalIdentity = "john@github.com";
TestRequest request = underTest.newRequest()
.setParam("login", userLogin)
.setParam("newExternalProvider", newExternalIdentityProvider)
.setParam("newExternalIdentity", newExternalIdentity);

request.execute();
assertThat(dbClient.userDao().selectByExternalLoginAndIdentityProvider(dbSession, newExternalIdentity, newExternalIdentityProvider))
.isNotNull()
.extracting(UserDto::isLocal, UserDto::getExternalLogin, UserDto::getExternalIdentityProvider)
.contains(false, newExternalIdentity, newExternalIdentityProvider);
}

@Test
public void fail_if_user_not_exist() {
TestRequest request = underTest.newRequest()
.setParam("login", "not-existing")
.setParam("newExternalProvider", "gitlab");

assertThatThrownBy(request::execute)
.isInstanceOf(NotFoundException.class)
.hasMessage("User 'not-existing' doesn't exist");
}

@Test
public void fail_if_identity_provider_not_exist() {
createUser(true, "login-1", "login-1", SQ_AUTHORITY);
TestRequest request = underTest.newRequest()
.setParam("login", "login-1")
.setParam("newExternalProvider", "not-existing");

assertThatThrownBy(request::execute)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Value of parameter 'newExternalProvider' (not-existing) must be one of: [github, gitlab, sonarqube]");
}

@Test
public void fail_if_anonymous() {
userSession.anonymous();
TestRequest request = underTest.newRequest()
.setParam("login", "not-existing")
.setParam("newExternalProvider", "something");

assertThatThrownBy(request::execute)
.isInstanceOf(UnauthorizedException.class);
}

@Test
public void fail_if_not_admin() {
userSession.logIn("some-user");
TestRequest request = underTest.newRequest()
.setParam("login", "not-existing")
.setParam("newExternalProvider", "something");

assertThatThrownBy(request::execute)
.isInstanceOf(ForbiddenException.class);
}

private void createUser(boolean local, String login, String externalLogin, String externalIdentityProvider) {
UserDto userDto = newUserDto()
.setEmail("john@email.com")
.setLogin(login)
.setName("John")
.setScmAccounts(newArrayList("jn"))
.setActive(true)
.setLocal(local)
.setExternalLogin(externalLogin)
.setExternalIdentityProvider(externalIdentityProvider);
dbClient.userDao().insert(dbSession, userDto);
userIndexer.commitAndIndex(dbSession, userDto);
}

}

+ 2
- 2
server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/UsersWsModuleTest.java Переглянути файл

@@ -34,7 +34,7 @@ public class UsersWsModuleTest {
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new UsersWsModule(new ConfigurationBridge(settings)).configure(container);
assertThat(container.size()).isEqualTo(2 + 14);
assertThat(container.size()).isEqualTo(2 + 15);
}

@Test
@@ -43,6 +43,6 @@ public class UsersWsModuleTest {

ComponentContainer container = new ComponentContainer();
new UsersWsModule(new ConfigurationBridge(settings)).configure(container);
assertThat(container.size()).isEqualTo(2 + 15);
assertThat(container.size()).isEqualTo(2 + 16);
}
}

+ 1
- 0
sonar-ws-generator/src/main/java/org/sonarqube/wsgenerator/ApiDefinitionDownloader.java Переглянути файл

@@ -43,6 +43,7 @@ public class ApiDefinitionDownloader {
Orchestrator orchestrator = builder
// Enable organizations ws
.setServerProperty("sonar.sonarcloud.enabled", "true")
.setServerProperty("sonar.forceAuthentication", "false")
.build();

orchestrator.start();

+ 3
- 1
sonar-ws/src/main/java/org/sonarqube/ws/client/user/UsersWsParameters.java Переглянути файл

@@ -27,9 +27,9 @@ public class UsersWsParameters {
public static final String ACTION_CREATE = "create";
public static final String ACTION_DEACTIVATE = "deactivate";
public static final String ACTION_UPDATE = "update";
public static final String ACTION_GROUPS = "groups";
public static final String ACTION_SKIP_ONBOARDING_TUTORIAL = "skip_onboarding_tutorial";
public static final String ACTION_CURRENT = "current";
public static final String ACTION_UPDATE_IDENTITY_PROVIDER = "update_identity_provider";

public static final String PARAM_LOGIN = "login";
public static final String PARAM_PASSWORD = "password";
@@ -40,6 +40,8 @@ public class UsersWsParameters {
public static final String PARAM_SCM_ACCOUNT = "scmAccount";
public static final String PARAM_LOCAL = "local";
public static final String PARAM_SELECTED = "selected";
public static final String PARAM_NEW_EXTERNAL_PROVIDER = "newExternalProvider";
public static final String PARAM_NEW_EXTERNAL_IDENTITY = "newExternalIdentity";

private UsersWsParameters() {
// Only static stuff

+ 72
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/users/UpdateIdentityProviderRequest.java Переглянути файл

@@ -0,0 +1,72 @@
/*
* SonarQube
* Copyright (C) 2009-2020 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarqube.ws.client.users;

import java.util.List;
import javax.annotation.Generated;

/**
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/users/update_identity_provider">Further information about this action online (including a response example)</a>
* @since 8.7
*/
@Generated("sonar-ws-generator")
public class UpdateIdentityProviderRequest {

private String login;
private String newExternalIdentity;
private String newExternalProvider;

/**
* This is a mandatory parameter.
*/
public UpdateIdentityProviderRequest setLogin(String login) {
this.login = login;
return this;
}

public String getLogin() {
return login;
}

/**
*/
public UpdateIdentityProviderRequest setNewExternalIdentity(String newExternalIdentity) {
this.newExternalIdentity = newExternalIdentity;
return this;
}

public String getNewExternalIdentity() {
return newExternalIdentity;
}

/**
* This is a mandatory parameter.
*/
public UpdateIdentityProviderRequest setNewExternalProvider(String newExternalProvider) {
this.newExternalProvider = newExternalProvider;
return this;
}

public String getNewExternalProvider() {
return newExternalProvider;
}
}

+ 17
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/users/UsersService.java Переглянути файл

@@ -227,4 +227,21 @@ public class UsersService extends BaseService {
.setParam("newLogin", request.getNewLogin())
.setMediaType(MediaTypes.JSON)).content();
}

/**
*
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/users/update_identity_provider">Further information about this action online (including a response example)</a>
* @since 8.7
*/
public void updateIdentityProvider(UpdateIdentityProviderRequest request) {
call(
new PostRequest(path("update_identity_provider"))
.setParam("login", request.getLogin())
.setParam("newExternalIdentity", request.getNewExternalIdentity())
.setParam("newExternalProvider", request.getNewExternalProvider())
.setMediaType(MediaTypes.JSON)
).content();
}
}

Завантаження…
Відмінити
Зберегти