import com.sonar.orchestrator.Orchestrator;
import it.actionPlan.ActionPlanTest;
import it.actionPlan.ActionPlanUiTest;
+import it.authorisation.AuthenticationTest;
import it.authorisation.IssuePermissionTest;
+import it.authorisation.PermissionTest;
import it.i18n.I18nTest;
import it.measureHistory.DifferentialPeriodsTest;
import it.measureHistory.HistoryUiTest;
QualityGateUiTest.class,
QualityGateNotificationTest.class,
// permission
+ AuthenticationTest.class,
+ PermissionTest.class,
IssuePermissionTest.class,
// measure history
DifferentialPeriodsTest.class,
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 it.authorisation;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.BuildResult;
+import com.sonar.orchestrator.build.SonarRunner;
+import com.sonar.orchestrator.locator.FileLocation;
+import it.Category1Suite;
+import java.util.UUID;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.sonarqube.ws.WsUserTokens;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.permission.AddGroupWsRequest;
+import org.sonarqube.ws.client.permission.AddUserWsRequest;
+import org.sonarqube.ws.client.permission.RemoveGroupWsRequest;
+import org.sonarqube.ws.client.usertoken.GenerateWsRequest;
+import org.sonarqube.ws.client.usertoken.UserTokensWsClient;
+
+import static java.lang.String.format;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonarqube.ws.client.HttpConnector.newHttpConnector;
+import static org.sonarqube.ws.client.WsRequest.newGetRequest;
+import static org.sonarqube.ws.client.WsRequest.newPostRequest;
+import static util.ItUtils.newAdminWsClient;
+import static util.ItUtils.projectDir;
+
+public class AuthenticationTest {
+ @ClassRule
+ public static Orchestrator ORCHESTRATOR = Category1Suite.ORCHESTRATOR;
+ private static WsClient adminWsClient;
+ private static UserTokensWsClient userTokensWsClient;
+
+ private static final String PROJECT_KEY = "sample";
+ private static final String LOGIN = "george.orwell";
+
+ @BeforeClass
+ public static void setUp() {
+ ORCHESTRATOR.resetData();
+ ORCHESTRATOR.getServer().restoreProfile(FileLocation.ofClasspath("/authorisation/one-issue-per-line-profile.xml"));
+ ORCHESTRATOR.getServer().provisionProject(PROJECT_KEY, "Sample");
+ ORCHESTRATOR.getServer().associateProjectToQualityProfile("sample", "xoo", "one-issue-per-line");
+
+ adminWsClient = newAdminWsClient(ORCHESTRATOR);
+ userTokensWsClient = adminWsClient.userTokensWsClient();
+ removeGroupPermission("anyone", "dryRunScan");
+ removeGroupPermission("anyone", "scan");
+
+ createUser(LOGIN, "123456");
+ addUserPermission(LOGIN, "admin");
+ addUserPermission(LOGIN, "scan");
+ }
+
+ @AfterClass
+ public static void delete_data() {
+ deactivateUser(LOGIN);
+ addGroupPermission("anyone", "dryRunScan");
+ addGroupPermission("anyone", "scan");
+ }
+
+ @Test
+ public void basic_authentication_based_on_login_and_password() {
+ String userId = UUID.randomUUID().toString();
+ String login = format("login-%s", userId);
+ String name = format("name-%s", userId);
+ String password = "!ascii-only:-)@";
+ createUser(login, name, password);
+
+ // authenticate
+ WsClient wsClient = new WsClient(newHttpConnector().url(ORCHESTRATOR.getServer().getUrl()).login(login).password(password).build());
+ String response = wsClient.execute(newGetRequest("api/authentication/validate"));
+ assertThat(response).isEqualTo("{\"valid\":true}");
+ }
+
+ @Test
+ public void basic_authentication_based_on_token() {
+ WsUserTokens.GenerateWsResponse generateWsResponse = userTokensWsClient.generate(new GenerateWsRequest()
+ .setLogin(LOGIN)
+ .setName("Validate token based authentication"));
+ WsClient wsClient = new WsClient(newHttpConnector()
+ .url(ORCHESTRATOR.getServer().getUrl())
+ .login(generateWsResponse.getToken())
+ .password("").build());
+
+ String response = wsClient.execute(newGetRequest("api/authentication/validate"));
+
+ assertThat(response).isEqualTo("{\"valid\":true}");
+ }
+
+ /**
+ * This is currently a limitation of Ruby on Rails stack.
+ */
+ @Test
+ public void basic_authentication_does_not_support_utf8_passwords() {
+ String userId = UUID.randomUUID().toString();
+ String login = format("login-%s", userId);
+ // see http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
+ String password = "κόσμε";
+
+ // create user with a UTF-8 password
+ createUser(login, format("name-%s", userId), password);
+
+ // authenticate
+ WsClient wsClient = new WsClient(newHttpConnector().url(ORCHESTRATOR.getServer().getUrl()).login(login).password(password).build());
+ String response = wsClient.execute(newGetRequest("api/authentication/validate"));
+ assertThat(response).isEqualTo("{\"valid\":false}");
+ }
+
+ @Test
+ @Ignore
+ public void web_login_form_should_support_utf8_passwords() {
+ // TODO selenium
+ }
+
+ @Test
+ public void run_analysis_with_token_authentication() {
+ WsUserTokens.GenerateWsResponse generateWsResponse = userTokensWsClient.generate(new GenerateWsRequest()
+ .setLogin(LOGIN)
+ .setName("Analyze Project"));
+ SonarRunner sampleProject = SonarRunner.create(projectDir("shared/xoo-sample"));
+ sampleProject.setProperties(
+ "sonar.login", generateWsResponse.getToken(),
+ "sonar.password", "");
+
+ BuildResult buildResult = ORCHESTRATOR.executeBuild(sampleProject);
+
+ assertThat(buildResult.isSuccess()).isTrue();
+ }
+
+ @Test
+ public void run_analysis_with_incorrect_token() {
+ SonarRunner sampleProject = SonarRunner.create(projectDir("shared/xoo-sample"));
+ sampleProject.setProperties(
+ "sonar.login", "unknown-token",
+ "sonar.password", "");
+
+ BuildResult buildResult = ORCHESTRATOR.executeBuildQuietly(sampleProject);
+
+ assertThat(buildResult.isSuccess()).isFalse();
+ }
+
+ private static void createUser(String login, String password) {
+ adminWsClient.execute(
+ newPostRequest("api/users/create")
+ .setParam("login", login)
+ .setParam("name", login)
+ .setParam("password", password));
+ }
+
+ private static void createUser(String login, String name, String password) {
+ adminWsClient.execute(
+ newPostRequest("api/users/create")
+ .setParam("login", login)
+ .setParam("name", name)
+ .setParam("password", password));
+ }
+
+ private static void addUserPermission(String login, String permission) {
+ adminWsClient.permissionsClient().addUser(new AddUserWsRequest()
+ .setLogin(login)
+ .setPermission(permission));
+ }
+
+ private static void deactivateUser(String login) {
+ adminWsClient.execute(
+ newPostRequest("api/users/deactivate")
+ .setParam("login", login));
+ }
+
+ private static void removeGroupPermission(String groupName, String permission) {
+ adminWsClient.permissionsClient().removeGroup(new RemoveGroupWsRequest()
+ .setGroupName(groupName)
+ .setPermission(permission));
+ }
+
+ private static void addGroupPermission(String groupName, String permission) {
+ adminWsClient.permissionsClient().addGroup(new AddGroupWsRequest()
+ .setGroupName(groupName)
+ .setPermission(permission));
+ }
+}
* subject to change in subsequent SonarQube versions.
* <br/>
* Length does not exceed 40 characters (arbitrary value).
+ * <br/>
+ * The token is sent through the userid field (login) of HTTP Basic authentication,
+ *
+ * Basic authentication is used to authenticate users from tokens, so the
+ * constraints of userid field (login) must be respected. Basically the token
+ * must not contain colon character ":".
+ *
*/
String generate();
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.usertoken;
+
+import com.google.common.base.Optional;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.user.UserTokenDto;
+
+public class UserTokenAuthenticator {
+ private final TokenGenerator tokenGenerator;
+ private final DbClient dbClient;
+
+ public UserTokenAuthenticator(TokenGenerator tokenGenerator, DbClient dbClient) {
+ this.tokenGenerator = tokenGenerator;
+ this.dbClient = dbClient;
+ }
+
+ /**
+ * Returns the user login if the token hash is found, else {@code Optional.absent()}.
+ * The returned login is not validated. If database is corrupted (table USER_TOKENS badly purged
+ * for instance), then the login may not relate to a valid user.
+ */
+ public Optional<String> authenticate(String token) {
+ String tokenHash = tokenGenerator.hash(token);
+ DbSession dbSession = dbClient.openSession(false);
+ try {
+ Optional<UserTokenDto> userToken = dbClient.userTokenDao().selectByTokenHash(dbSession, tokenHash);
+ if (userToken.isPresent()) {
+ return Optional.of(userToken.get().getLogin());
+ }
+ return Optional.absent();
+ } finally {
+ dbClient.closeSession(dbSession);
+ }
+ }
+}
add(
UserTokensWs.class,
GenerateAction.class,
+ UserTokenAuthenticator.class,
TokenGeneratorImpl.class);
}
}
.hasSize(40);
}
+ @Test
+ public void token_does_not_contain_colon() {
+ assertThat(underTest.generate()).doesNotContain(":");
+ }
+
@Test
public void hash_token() {
String hash = underTest.hash("1234567890123456789012345678901234567890");
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.usertoken;
+
+import com.google.common.base.Optional;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.test.DbTests;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.db.user.UserTokenTesting.newUserToken;
+
+@Category(DbTests.class)
+public class UserTokenAuthenticatorTest {
+ static final String GRACE_HOPPER = "grace.hopper";
+ static final String ADA_LOVELACE = "ada.lovelace";
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Rule
+ public DbTester db = DbTester.create(System2.INSTANCE);
+ DbClient dbClient = db.getDbClient();
+ DbSession dbSession = db.getSession();
+ TokenGenerator tokenGenerator = mock(TokenGenerator.class);
+
+ UserTokenAuthenticator underTest = new UserTokenAuthenticator(tokenGenerator, db.getDbClient());
+
+ @Test
+ public void return_login_when_token_hash_found_in_db() {
+ String token = "known-token";
+ String tokenHash = "123456789";
+ when(tokenGenerator.hash(token)).thenReturn(tokenHash);
+ dbClient.userTokenDao().insert(dbSession, newUserToken().setLogin(GRACE_HOPPER).setTokenHash(tokenHash));
+ dbClient.userTokenDao().insert(dbSession, newUserToken().setLogin(ADA_LOVELACE).setTokenHash("another-token-hash"));
+ db.commit();
+
+ Optional<String> login = underTest.authenticate(token);
+
+ assertThat(login.isPresent()).isTrue();
+ assertThat(login.get()).isEqualTo(GRACE_HOPPER);
+ }
+
+ @Test
+ public void return_absent_if_token_hash_is_not_found() {
+ Optional<String> login = underTest.authenticate("unknown-token");
+ assertThat(login.isPresent()).isFalse();
+ }
+}
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new UserTokenModule().configure(container);
- assertThat(container.size()).isEqualTo(5);
+ assertThat(container.size()).isEqualTo(6);
}
}
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.experimental.categories.Category;
import org.junit.rules.ExpectedException;
import org.sonar.api.utils.System2;
import org.sonar.core.permission.GlobalPermissions;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.usertoken.TokenGenerator;
import org.sonar.server.ws.WsActionTester;
+import org.sonar.test.DbTests;
import org.sonarqube.ws.MediaTypes;
import org.sonarqube.ws.WsUserTokens.GenerateWsResponse;
import static org.sonarqube.ws.client.usertoken.UserTokensWsParameters.PARAM_LOGIN;
import static org.sonarqube.ws.client.usertoken.UserTokensWsParameters.PARAM_NAME;
+@Category(DbTests.class)
public class GenerateActionTest {
private static final String GRACE_HOPPER = "grace.hopper";
private static final String ADA_LOVELACE = "ada.lovelace";
# prevents a user from submitting a crafted form that bypasses activation
# anything else you want your user to change should be added here.
attr_accessible :login, :email, :name, :password, :password_confirmation
+ attr_accessor :token_authenticated
####
# As now dates are saved in long they should be no more automatically managed by Rails
# Called from #current_user. Now, attempt to login by basic authentication information.
def login_from_basic_auth
authenticate_with_http_basic do |login, password|
- self.current_user = User.authenticate(login, password, servlet_request)
+ # The access token is sent as the login of Basic authentication. To distinguish with regular logins,
+ # the convention is that the password is empty
+ if password.empty? && login.present?
+ # authentication by access token
+ token_authenticator = Java::OrgSonarServerPlatform::Platform.component(Java::OrgSonarServerUsertoken::UserTokenAuthenticator.java_class)
+ authenticated_login = token_authenticator.authenticate(login)
+ if authenticated_login.isPresent()
+ user = User.find_active_by_login(authenticated_login.get())
+ if user
+ user.token_authenticated=true
+ self.current_user = user
+ self.current_user
+ end
+ end
+ else
+ # regular Basic authentication with login and password
+ self.current_user = User.authenticate(login, password, servlet_request)
+ end
end
end