import org.junit.Test;
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.user.CreateRequest;
import util.user.UserRule;
import util.user.Users;
setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
userRule.createUser("another", "Another", USER_EMAIL, "another");
- runSelenese(ORCHESTRATOR,"/user/BaseIdentityProviderTest/fail_when_email_already_exists.html");
+ runSelenese(ORCHESTRATOR, "/user/BaseIdentityProviderTest/fail_when_email_already_exists.html");
File logFile = ORCHESTRATOR.getServer().getWebLogs();
- assertThat(FileUtils.readFileToString(logFile)).doesNotContain("You can't sign up because email 'john@email.com' is already used by an existing user. This means that you probably already registered with another account");
+ assertThat(FileUtils.readFileToString(logFile))
+ .doesNotContain("You can't sign up because email 'john@email.com' is already used by an existing user. This means that you probably already registered with another account");
}
@Test
setServerProperty(ORCHESTRATOR, "sonar.auth.fake-base-id-provider.throwUnauthorizedMessage", "true");
runSelenese(ORCHESTRATOR,
- "/user/BaseIdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html");
+ "/user/BaseIdentityProviderTest/display_message_in_ui_but_not_in_log_when_unauthorized_exception.html");
File logFile = ORCHESTRATOR.getServer().getWebLogs();
assertThat(FileUtils.readFileToString(logFile)).doesNotContain("A functional error has happened");
userRule.verifyUserExists(login, USER_NAME, USER_EMAIL, false);
}
+ @Test
+ public void provision_user_before_authentication() {
+ enablePlugin();
+ setUserCreatedByAuthPlugin(USER_LOGIN, USER_PROVIDER_ID, USER_NAME, USER_EMAIL);
+
+ // Provision none local user in database
+ newAdminWsClient(ORCHESTRATOR).users().create(CreateRequest.builder()
+ .setLogin(USER_LOGIN)
+ .setName(USER_NAME)
+ .setEmail(USER_EMAIL)
+ .setLocal(false)
+ .build());
+ assertThat(userRule.getUserByLogin(USER_LOGIN).get())
+ .extracting(Users.User::isLocal, Users.User::getExternalIdentity, Users.User::getExternalProvider)
+ .containsOnly(false, USER_LOGIN, "sonarqube");
+
+ // Authenticate with external system -> It will update external provider info
+ authenticateWithFakeAuthProvider();
+
+ assertThat(userRule.getUserByLogin(USER_LOGIN).get())
+ .extracting(Users.User::isLocal, Users.User::getExternalIdentity, Users.User::getExternalProvider)
+ .containsOnly(false, USER_PROVIDER_ID, FAKE_PROVIDER_KEY);
+ }
+
private static void enablePlugin() {
setServerProperty(ORCHESTRATOR, "sonar.auth.fake-base-id-provider.enabled", "true");
}
private static void authenticateWithFakeAuthProvider() {
adminWsClient.wsConnector().call(
- new GetRequest(("/sessions/init/" + FAKE_PROVIDER_KEY)))
+ new GetRequest("/sessions/init/" + FAKE_PROVIDER_KEY))
.failIfNotSuccessful();
}
import org.sonarqube.ws.client.GetRequest;
import org.sonarqube.ws.client.WsClient;
import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.user.CreateRequest;
import util.user.UserRule;
+import util.user.Users;
import static org.assertj.core.api.Assertions.assertThat;
import static util.ItUtils.newAdminWsClient;
assertThat(FileUtils.readFileToString(logFile)).doesNotContain("You can't sign up because email 'john@email.com' is already used by an existing user. This means that you probably already registered with another account");
}
+ @Test
+ public void provision_user_before_authentication() {
+ simulateRedirectionToCallback();
+ enablePlugin();
+
+ // Provision none local user in database
+ newAdminWsClient(ORCHESTRATOR).users().create(CreateRequest.builder()
+ .setLogin(USER_LOGIN)
+ .setName(USER_NAME)
+ .setEmail(USER_EMAIL)
+ .setLocal(false)
+ .build());
+ assertThat(userRule.getUserByLogin(USER_LOGIN).get())
+ .extracting(Users.User::isLocal, Users.User::getExternalIdentity, Users.User::getExternalProvider)
+ .containsOnly(false, USER_LOGIN, "sonarqube");
+
+ // Authenticate with external system -> It will update external provider info
+ authenticateWithFakeAuthProvider();
+
+ assertThat(userRule.getUserByLogin(USER_LOGIN).get())
+ .extracting(Users.User::isLocal, Users.User::getExternalIdentity, Users.User::getExternalProvider)
+ .containsOnly(false, USER_PROVIDER_ID, FAKE_PROVIDER_KEY);
+ }
+
private void authenticateWithFakeAuthProvider() {
WsResponse response = adminWsClient.wsConnector().call(
new GetRequest(("/sessions/init/" + FAKE_PROVIDER_KEY)));
import org.sonar.wsclient.services.AuthenticationQuery;
import org.sonar.wsclient.user.UserParameters;
import org.sonarqube.ws.client.GetRequest;
-import org.sonarqube.ws.client.HttpConnector;
-import org.sonarqube.ws.client.WsClient;
-import org.sonarqube.ws.client.WsClientFactories;
import org.sonarqube.ws.client.WsResponse;
+import org.sonarqube.ws.client.user.CreateRequest;
import util.user.UserRule;
+import util.user.Users;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.newUserWsClient;
import static util.ItUtils.pluginArtifact;
import static util.ItUtils.setServerProperty;
import static util.selenium.Selenese.runSelenese;
// Then
verifyAuthenticationIsOk(login, password);
-
// When external system does not work
users.remove(login + ".password");
updateUsersInExtAuth(users);
updateUsersInExtAuth(ImmutableMap.of(username + ".password", password));
verifyAuthenticationIsOk(username, password);
- ;
}
- protected void verifyHttpException(Exception e, int expectedCode) {
+ @Test
+ public void provision_user_before_authentication() {
+ newAdminWsClient(orchestrator).users().create(CreateRequest.builder()
+ .setLogin(USER_LOGIN)
+ .setName("Tester Testerovich")
+ .setEmail("tester@example.org")
+ .setLocal(false)
+ .build());
+ // The user is created in SonarQube but doesn't exist yet in external authentication system
+ verifyAuthenticationIsNotOk(USER_LOGIN, "123");
+
+ updateUsersInExtAuth(ImmutableMap.of(
+ USER_LOGIN + ".password", "123",
+ USER_LOGIN + ".name", "Tester Testerovich",
+ USER_LOGIN + ".email", "tester@example.org"));
+
+ verifyAuthenticationIsOk(USER_LOGIN, "123");
+ assertThat(USER_RULE.getUserByLogin(USER_LOGIN).get())
+ .extracting(Users.User::isLocal, Users.User::getExternalIdentity, Users.User::getExternalProvider)
+ .containsOnly(false, USER_LOGIN, "sonarqube");
+ }
+
+ private void verifyHttpException(Exception e, int expectedCode) {
assertThat(e).isInstanceOf(HttpException.class);
HttpException exception = (HttpException) e;
assertThat(exception.status()).isEqualTo(expectedCode);
}
private WsResponse checkAuthenticationWithWebService(String login, String password) {
- WsClient wsClient = WsClientFactories.getDefault().newClient(HttpConnector.newBuilder().url(orchestrator.getServer().getUrl()).credentials(login, password).build());
// Call any WS
- return wsClient.wsConnector().call(new GetRequest("api/rules/search"));
+ return newUserWsClient(orchestrator, login, password).wsConnector().call(new GetRequest("api/rules/search"));
}
}
private final String login;
private final String name;
private final String email;
+ private final String externalIdentity;
+ private final String externalProvider;
private final List<String> groups;
private final List<String> scmAccounts;
private final boolean active;
private final boolean local;
private int tokensCount;
- private User(String login, String name, String email, List<String> groups, List<String> scmAccounts, boolean active, boolean local, int tokensCount) {
+ private User(String login, String name, String email, String externalIdentity, String externalProvider, List<String> groups, List<String> scmAccounts,
+ boolean active, boolean local, int tokensCount) {
this.login = login;
this.name = name;
+ this.externalIdentity = externalIdentity;
+ this.externalProvider = externalProvider;
this.email = email;
this.groups = groups;
this.scmAccounts = scmAccounts;
public int getTokensCount() {
return tokensCount;
}
- }
-}
+ public String getExternalIdentity() {
+ return externalIdentity;
+ }
+ public String getExternalProvider() {
+ return externalProvider;
+ }
+ }
+}
import static org.apache.commons.lang.StringUtils.trimToNull;
import static org.sonar.api.CoreProperties.CORE_AUTHENTICATOR_CREATE_USERS;
import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
-import static org.sonar.server.user.UserUpdater.SQ_AUTHORITY;
+import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
public class RealmAuthenticator implements Startable {
import static org.apache.commons.lang.StringUtils.defaultIfBlank;
import static org.apache.commons.lang.time.DateUtils.addMinutes;
import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
-import static org.sonar.server.user.UserUpdater.SQ_AUTHORITY;
+import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
public class SsoAuthenticator implements Startable {
import static java.util.Objects.requireNonNull;
public class ExternalIdentity {
+
+ public static final String SQ_AUTHORITY = "sonarqube";
+
private String provider;
private String id;
import org.sonar.api.server.ws.WebService;
import org.sonar.core.permission.GlobalPermissions;
import org.sonar.db.user.UserDto;
+import org.sonar.server.user.ExternalIdentity;
import org.sonar.server.user.NewUser;
import org.sonar.server.user.UserSession;
import org.sonar.server.user.UserUpdater;
import static com.google.common.base.Strings.emptyToNull;
import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
import static org.sonar.server.ws.WsUtils.writeProtobuf;
import static org.sonarqube.ws.client.user.UsersWsParameters.ACTION_CREATE;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_EMAIL;
+import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_LOCAL;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_LOGIN;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_NAME;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_PASSWORD;
.setDescription("Create a user.<br/>" +
"If a deactivated user account exists with the given login, it will be reactivated.<br/>" +
"Requires Administer System permission<br/>" +
+ "Since 6.3, the password is only mandatory when creating local users, and should not be set on non local users<br/>" +
"Since 6.3, the 'infos' message is no more returned when a user is reactivated")
.setSince("3.7")
.setPost(true)
.setExampleValue("myuser");
action.createParam(PARAM_PASSWORD)
- .setDescription("User password")
- .setRequired(true)
+ .setDescription("User password. Only mandatory when creating local user, otherwise it should not be set")
.setExampleValue("mypassword");
action.createParam(PARAM_NAME)
action.createParam(PARAM_SCM_ACCOUNT)
.setDescription("SCM accounts. To set several values, the parameter must be called once for each value.")
.setExampleValue("scmAccount=firstValue&scmAccount=secondValue&scmAccount=thirdValue");
+
+ action.createParam(PARAM_LOCAL)
+ .setDescription("Specify if the user should be authenticated from SonarQube server or from an external authentication system. " +
+ "Password should not be set when local is set to false.")
+ .setSince("6.3")
+ .setDefaultValue("true")
+ .setBooleanPossibleValues();
}
@Override
.setEmail(request.getEmail())
.setScmAccounts(request.getScmAccounts())
.setPassword(request.getPassword());
+ if (!request.isLocal()) {
+ newUser.setExternalIdentity(new ExternalIdentity(SQ_AUTHORITY, request.getLogin()));
+ }
UserDto userDto = userUpdater.create(newUser.build());
return buildResponse(userDto);
}
private static CreateRequest toWsRequest(Request request) {
return CreateRequest.builder()
.setLogin(request.mandatoryParam(PARAM_LOGIN))
- .setPassword(request.mandatoryParam(PARAM_PASSWORD))
+ .setPassword(request.param(PARAM_PASSWORD))
.setName(request.param(PARAM_NAME))
.setEmail(request.param(PARAM_EMAIL))
.setScmAccounts(getScmAccounts(request))
+ .setLocal(request.mandatoryParamAsBoolean(PARAM_LOCAL))
.build();
}
import static org.sonar.api.CoreProperties.CORE_DEFAULT_GROUP;
import static org.sonar.db.user.UserTesting.newDisabledUser;
import static org.sonar.db.user.UserTesting.newUserDto;
+import static org.sonar.server.user.ExternalIdentity.SQ_AUTHORITY;
public class UserUpdaterTest {
assertThat(dto.isLocal()).isTrue();
}
+ @Test
+ public void create_user_with_identity_provider() throws Exception {
+ createDefaultGroup();
+
+ underTest.create(NewUser.builder()
+ .setLogin("user")
+ .setName("User")
+ .setExternalIdentity(new ExternalIdentity("github", "github-user"))
+ .build());
+
+ UserDto dto = dbClient.userDao().selectByLogin(session, "user");
+ assertThat(dto.isLocal()).isFalse();
+ assertThat(dto.getExternalIdentity()).isEqualTo("github-user");
+ assertThat(dto.getExternalIdentityProvider()).isEqualTo("github");
+ assertThat(dto.getCryptedPassword()).isNull();
+ assertThat(dto.getSalt()).isNull();
+ }
+
+ @Test
+ public void create_user_with_sonarqube_external_identity() throws Exception {
+ createDefaultGroup();
+
+ underTest.create(NewUser.builder()
+ .setLogin("user")
+ .setName("User")
+ .setExternalIdentity(new ExternalIdentity(SQ_AUTHORITY, "user"))
+ .build());
+
+ UserDto dto = dbClient.userDao().selectByLogin(session, "user");
+ assertThat(dto.isLocal()).isFalse();
+ assertThat(dto.getExternalIdentity()).isEqualTo("user");
+ assertThat(dto.getExternalIdentityProvider()).isEqualTo("sonarqube");
+ assertThat(dto.getCryptedPassword()).isNull();
+ assertThat(dto.getSalt()).isNull();
+ }
+
@Test
public void create_user_with_minimum_fields() {
when(system2.now()).thenReturn(1418215735482L);
assertThat(db.users().selectGroupIdsOfUser(dbUser.get())).containsOnly(defaultGroupInDefaultOrg.getId());
}
+ @Test
+ public void create_local_user() throws Exception {
+ authenticateAsAdmin();
+
+ call(CreateRequest.builder()
+ .setLogin("john")
+ .setName("John")
+ .setPassword("1234")
+ .setLocal(true)
+ .build());
+
+ assertThat(db.users().selectUserByLogin("john").get())
+ .extracting(UserDto::isLocal, UserDto::getExternalIdentityProvider, UserDto::getExternalIdentity)
+ .containsOnly(true, "sonarqube", "john");
+ }
+
+ @Test
+ public void create_none_local_user() throws Exception {
+ authenticateAsAdmin();
+
+ call(CreateRequest.builder()
+ .setLogin("john")
+ .setName("John")
+ .setLocal(false)
+ .build());
+
+ assertThat(db.users().selectUserByLogin("john").get())
+ .extracting(UserDto::isLocal, UserDto::getExternalIdentityProvider, UserDto::getExternalIdentity)
+ .containsOnly(false, "sonarqube", "john");
+ }
+
@Test
public void create_user_with_comma_in_scm_account() throws Exception {
authenticateAsAdmin();
settings.setProperty("sonar.defaultGroup", adminGroup.getName());
}
+ @Test
+ public void fail_when_missing_login() throws Exception {
+ authenticateAsAdmin();
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Login is mandatory and must not be empty");
+ call(CreateRequest.builder()
+ .setLogin(null)
+ .setName("John")
+ .setPassword("1234")
+ .build());
+ }
+
+ @Test
+ public void fail_when_missing_name() throws Exception {
+ authenticateAsAdmin();
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Name is mandatory and must not be empty");
+ call(CreateRequest.builder()
+ .setLogin("john")
+ .setName(null)
+ .setPassword("1234")
+ .build());
+ }
+
+ @Test
+ public void fail_when_missing_password() throws Exception {
+ authenticateAsAdmin();
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Password is mandatory and must not be empty");
+ call(CreateRequest.builder()
+ .setLogin("john")
+ .setName("John")
+ .setPassword(null)
+ .build());
+ }
+
+ @Test
+ public void fail_when_password_is_set_on_none_local_user() throws Exception {
+ authenticateAsAdmin();
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Password should only be set on local user");
+ call(CreateRequest.builder()
+ .setLogin("john")
+ .setName("John")
+ .setPassword("1234")
+ .setLocal(false)
+ .build());
+ }
+
@Test
public void fail_on_missing_permission() throws Exception {
userSessionRule.logIn("not_admin");
setNullable(createRequest.getEmail(), e -> request.setParam("email", e));
setNullable(createRequest.getPassword(), e -> request.setParam("password", e));
setNullable(createRequest.getScmAccounts(), e -> request.setMultiParam("scmAccount", e));
+ request.setParam("local", createRequest.isLocal() ? "true" : "false");
try {
return CreateWsResponse.parseFrom(request.execute().getInputStream());
} catch (IOException e) {
WebService.Action action = controller.action("create");
assertThat(action).isNotNull();
assertThat(action.isPost()).isTrue();
- assertThat(action.params()).hasSize(6);
+ assertThat(action.params()).hasSize(7);
}
@Test
private final String name;
private final String email;
private final List<String> scmAccounts;
+ private final boolean local;
private CreateRequest(Builder builder) {
this.login = builder.login;
this.name = builder.name;
this.email = builder.email;
this.scmAccounts = builder.scmAccounts;
+ this.local = builder.local;
}
public String getLogin() {
return login;
}
+ @CheckForNull
public String getPassword() {
return password;
}
return scmAccounts;
}
+ public boolean isLocal() {
+ return local;
+ }
+
public static Builder builder() {
return new Builder();
}
private String name;
private String email;
private List<String> scmAccounts = emptyList();
+ private boolean local = true;
private Builder() {
// enforce factory method use
return this;
}
- public Builder setPassword(String password) {
+ public Builder setPassword(@Nullable String password) {
this.password = password;
return this;
}
return this;
}
+ public Builder setLocal(boolean local) {
+ this.local = local;
+ return this;
+ }
+
public CreateRequest build() {
checkArgument(!isNullOrEmpty(login), "Login is mandatory and must not be empty");
- checkArgument(!isNullOrEmpty(password), "Password is mandatory and must not be empty");
checkArgument(!isNullOrEmpty(name), "Name is mandatory and must not be empty");
+ checkArgument(!local || !isNullOrEmpty(password), "Password is mandatory and must not be empty");
+ checkArgument(local || isNullOrEmpty(password), "Password should only be set on local user");
return new CreateRequest(this);
}
}
import static org.sonarqube.ws.client.user.UsersWsParameters.ACTION_UPDATE;
import static org.sonarqube.ws.client.user.UsersWsParameters.CONTROLLER_USERS;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_EMAIL;
+import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_LOCAL;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_LOGIN;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_NAME;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_PASSWORD;
.setParam(PARAM_PASSWORD, request.getPassword())
.setParam(PARAM_NAME, request.getName())
.setParam(PARAM_EMAIL, request.getEmail())
- .setParam(PARAM_SCM_ACCOUNT, request.getScmAccounts()),
+ .setParam(PARAM_SCM_ACCOUNT, request.getScmAccounts())
+ .setParam(PARAM_LOCAL, request.isLocal()),
CreateWsResponse.parser());
}
public static final String PARAM_SCM_ACCOUNTS = "scmAccounts";
public static final String PARAM_SCM_ACCOUNTS_DEPRECATED = "scm_accounts";
public static final String PARAM_SCM_ACCOUNT = "scmAccount";
+ public static final String PARAM_LOCAL = "local";
private UsersWsParameters() {
// Only static stuff
assertThat(result.getName()).isEqualTo("John");
assertThat(result.getEmail()).isEqualTo("john@doo.com");
assertThat(result.getScmAccounts()).containsOnly("jo", "hn");
+ assertThat(result.isLocal()).isTrue();
+ }
+
+ @Test
+ public void create_request_for_local_user() {
+ CreateRequest result = underTest
+ .setLogin("john")
+ .setPassword("123456")
+ .setName("John")
+ .setLocal(true)
+ .build();
+
+ assertThat(result.isLocal()).isTrue();
+ }
+
+ @Test
+ public void create_request_for_none_local_user() {
+ CreateRequest result = underTest
+ .setLogin("john")
+ .setName("John")
+ .setLocal(false)
+ .build();
+
+ assertThat(result.isLocal()).isFalse();
}
@Test
}
@Test
- public void fail_when_empty_password() {
+ public void fail_when_empty_password_on_local_user() {
expectedException.expect(IllegalArgumentException.class);
underTest
.setLogin("john")
.build();
}
+ @Test
+ public void fail_when_password_is_set_on_none_local_user() {
+ expectedException.expect(IllegalArgumentException.class);
+ underTest
+ .setLogin("john")
+ .setPassword("12345")
+ .setName("12345")
+ .setLocal(false)
+ .build();
+ }
+
}
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_EMAIL;
+import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_LOCAL;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_LOGIN;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_NAME;
import static org.sonarqube.ws.client.user.UsersWsParameters.PARAM_PASSWORD;
.setName("John")
.setEmail("john@doo.com")
.setScmAccounts(asList("jo", "hn"))
+ .setLocal(true)
.build());
assertThat(serviceTester.getPostParser()).isSameAs(CreateWsResponse.parser());
.hasParam(PARAM_NAME, "John")
.hasParam(PARAM_EMAIL, "john@doo.com")
.hasParam(PARAM_SCM_ACCOUNT, asList("jo", "hn"))
+ .hasParam(PARAM_LOCAL, "true")
.andNoOtherParam();
}