]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7054 Token based authentication 650/head
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Mon, 23 Nov 2015 15:55:20 +0000 (16:55 +0100)
committerTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Tue, 24 Nov 2015 16:45:56 +0000 (17:45 +0100)
it/it-tests/src/test/java/it/Category1Suite.java
it/it-tests/src/test/java/it/authorisation/AuthenticationTest.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/usertoken/TokenGenerator.java
server/sonar-server/src/main/java/org/sonar/server/usertoken/UserTokenAuthenticator.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/usertoken/UserTokenModule.java
server/sonar-server/src/test/java/org/sonar/server/usertoken/TokenGeneratorImplTest.java
server/sonar-server/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticatorTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/usertoken/UserTokenModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/usertoken/ws/GenerateActionTest.java
server/sonar-web/src/main/webapp/WEB-INF/app/models/user.rb
server/sonar-web/src/main/webapp/WEB-INF/lib/authenticated_system.rb

index 6896d5b070d48c6dba9908d69f8e7dc098bf25af..bc011b61cfd99f004f4bfc276c1e4d8241a132a1 100644 (file)
@@ -41,7 +41,9 @@ package it;/*
 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;
@@ -85,6 +87,8 @@ import static util.ItUtils.xooPlugin;
   QualityGateUiTest.class,
   QualityGateNotificationTest.class,
   // permission
+  AuthenticationTest.class,
+  PermissionTest.class,
   IssuePermissionTest.class,
   // measure history
   DifferentialPeriodsTest.class,
diff --git a/it/it-tests/src/test/java/it/authorisation/AuthenticationTest.java b/it/it-tests/src/test/java/it/authorisation/AuthenticationTest.java
new file mode 100644 (file)
index 0000000..30d0bdd
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * 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));
+  }
+}
index 46d70784021ed0412b4d41e307b04da6a993506a..0fa1c559341f74ff7ae6227faf187a406239d094 100644 (file)
@@ -27,6 +27,13 @@ public interface TokenGenerator {
    * 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();
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/usertoken/UserTokenAuthenticator.java b/server/sonar-server/src/main/java/org/sonar/server/usertoken/UserTokenAuthenticator.java
new file mode 100644 (file)
index 0000000..4d2bb22
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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);
+    }
+  }
+}
index 373264f20182da8bca32debd5b40f5a038fc646b..2fd1753fe2ca79793abdd4d028acd28428f97939 100644 (file)
@@ -30,6 +30,7 @@ public class UserTokenModule extends Module {
     add(
       UserTokensWs.class,
       GenerateAction.class,
+      UserTokenAuthenticator.class,
       TokenGeneratorImpl.class);
   }
 }
index bce967c2a3a55a14332df0f4d923027b80fcacf7..88d75bf33ab5c4e6550a4284b3e71dc25e194571 100644 (file)
@@ -39,6 +39,11 @@ public class TokenGeneratorImplTest {
       .hasSize(40);
   }
 
+  @Test
+  public void token_does_not_contain_colon() {
+    assertThat(underTest.generate()).doesNotContain(":");
+  }
+
   @Test
   public void hash_token() {
     String hash = underTest.hash("1234567890123456789012345678901234567890");
diff --git a/server/sonar-server/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/usertoken/UserTokenAuthenticatorTest.java
new file mode 100644 (file)
index 0000000..1a291c4
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * 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();
+  }
+}
index 91b9de139d68e0597a5488304896403665273981..e4bd3a614c5a0f5ad524ab2b62db2bcc13a4375c 100644 (file)
@@ -30,6 +30,6 @@ public class UserTokenModuleTest {
   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);
   }
 }
index aad9cc18cf15f44d80affd9fbc35e8e696a1ba22..a4190e479ef8541ec3372b7d6a033951e2cbfee3 100644 (file)
@@ -25,6 +25,7 @@ import java.io.InputStream;
 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;
@@ -36,6 +37,7 @@ import org.sonar.server.exceptions.ServerException;
 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;
 
@@ -49,6 +51,7 @@ import static org.sonar.test.JsonAssert.assertJson;
 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";
index fed2167f346f069417f09a37f282731b985c69ae..e6e3e2dc7ccecf525b12e1c53a02a9244052dece 100644 (file)
@@ -62,6 +62,7 @@ class User < ActiveRecord::Base
   # 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
index 0ec3518fb2f5e47107b810d193383c1873fa55c8..8f28272d4a0b2caea0263cf6d566ef468942c535 100644 (file)
@@ -127,7 +127,24 @@ module AuthenticatedSystem
   # 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