aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-auth-common
diff options
context:
space:
mode:
authorJulien Lancelot <julien.lancelot@sonarsource.com>2019-09-24 11:44:20 +0200
committerSonarTech <sonartech@sonarsource.com>2019-10-07 20:21:06 +0200
commit42f0cff638b6b7055acc6cf75bbb2215867d0474 (patch)
tree9c4bbdb045a1cabe844c14fdda2413c1b94a7fcb /server/sonar-auth-common
parentdaf5a60dd259039b97fd3598f894169d7ecc74e5 (diff)
downloadsonarqube-42f0cff638b6b7055acc6cf75bbb2215867d0474.tar.gz
sonarqube-42f0cff638b6b7055acc6cf75bbb2215867d0474.zip
SONAR-12471 Embed GitHub authentication
Diffstat (limited to 'server/sonar-auth-common')
-rw-r--r--server/sonar-auth-common/build.gradle21
-rw-r--r--server/sonar-auth-common/src/main/java/org/sonar/auth/OAuthRestClient.java99
-rw-r--r--server/sonar-auth-common/src/main/java/org/sonar/auth/package-info.java23
-rw-r--r--server/sonar-auth-common/src/test/java/org/sonar/auth/OAuthRestClientTest.java127
4 files changed, 270 insertions, 0 deletions
diff --git a/server/sonar-auth-common/build.gradle b/server/sonar-auth-common/build.gradle
new file mode 100644
index 00000000000..9a669541dac
--- /dev/null
+++ b/server/sonar-auth-common/build.gradle
@@ -0,0 +1,21 @@
+description = 'SonarQube :: Authentication :: Common'
+
+configurations {
+ testCompile.extendsFrom compileOnly
+}
+
+dependencies {
+ // please keep the list ordered
+
+ compile 'com.github.scribejava:scribejava-apis'
+ compile 'com.github.scribejava:scribejava-core'
+
+ compileOnly 'com.google.code.findbugs:jsr305'
+
+ testCompile 'commons-lang:commons-lang'
+ testCompile 'com.squareup.okhttp3:mockwebserver'
+ testCompile 'com.squareup.okhttp3:okhttp'
+ testCompile 'junit:junit'
+ testCompile 'org.assertj:assertj-core'
+ testCompile 'org.mockito:mockito-core'
+}
diff --git a/server/sonar-auth-common/src/main/java/org/sonar/auth/OAuthRestClient.java b/server/sonar-auth-common/src/main/java/org/sonar/auth/OAuthRestClient.java
new file mode 100644
index 00000000000..f07b9e0b595
--- /dev/null
+++ b/server/sonar-auth-common/src/main/java/org/sonar/auth/OAuthRestClient.java
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.auth;
+
+import com.github.scribejava.core.model.OAuth2AccessToken;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Response;
+import com.github.scribejava.core.model.Verb;
+import com.github.scribejava.core.oauth.OAuth20Service;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.lang.String.format;
+
+public class OAuthRestClient {
+
+ private static final int DEFAULT_PAGE_SIZE = 100;
+ private static final Pattern NEXT_LINK_PATTERN = Pattern.compile(".*<(.*)>; rel=\"next\"");
+
+ private OAuthRestClient() {
+ // Only static method
+ }
+
+ public static Response executeRequest(String requestUrl, OAuth20Service scribe, OAuth2AccessToken accessToken) throws IOException {
+ OAuthRequest request = new OAuthRequest(Verb.GET, requestUrl);
+ scribe.signRequest(accessToken, request);
+ try {
+ Response response = scribe.execute(request);
+ if (!response.isSuccessful()) {
+ throw unexpectedResponseCode(requestUrl, response);
+ }
+ return response;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(e);
+ } catch (ExecutionException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public static <E> List<E> executePaginatedRequest(String request, OAuth20Service scribe, OAuth2AccessToken accessToken, Function<String, List<E>> function) {
+ List<E> result = new ArrayList<>();
+ readPage(result, scribe, accessToken, request + "?per_page=" + DEFAULT_PAGE_SIZE, function);
+ return result;
+ }
+
+ private static <E> void readPage(List<E> result, OAuth20Service scribe, OAuth2AccessToken accessToken, String endPoint, Function<String, List<E>> function) {
+ try (Response nextResponse = executeRequest(endPoint, scribe, accessToken)) {
+ String content = nextResponse.getBody();
+ if (content == null) {
+ return;
+ }
+ result.addAll(function.apply(content));
+ readNextEndPoint(nextResponse).ifPresent(newNextEndPoint -> readPage(result, scribe, accessToken, newNextEndPoint, function));
+ } catch (IOException e) {
+ throw new IllegalStateException(format("Failed to get %s", endPoint), e);
+ }
+ }
+
+ private static Optional<String> readNextEndPoint(Response response) {
+ String link = response.getHeader("Link");
+ if (link == null || link.isEmpty() || !link.contains("rel=\"next\"")) {
+ return Optional.empty();
+ }
+ Matcher nextLinkMatcher = NEXT_LINK_PATTERN.matcher(link);
+ if (!nextLinkMatcher.find()) {
+ return Optional.empty();
+ }
+ return Optional.of(nextLinkMatcher.group(1));
+ }
+
+ private static IllegalStateException unexpectedResponseCode(String requestUrl, Response response) throws IOException {
+ return new IllegalStateException(format("Fail to execute request '%s'. HTTP code: %s, response: %s", requestUrl, response.getCode(), response.getBody()));
+ }
+
+}
diff --git a/server/sonar-auth-common/src/main/java/org/sonar/auth/package-info.java b/server/sonar-auth-common/src/main/java/org/sonar/auth/package-info.java
new file mode 100644
index 00000000000..00a16681355
--- /dev/null
+++ b/server/sonar-auth-common/src/main/java/org/sonar/auth/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.auth;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-auth-common/src/test/java/org/sonar/auth/OAuthRestClientTest.java b/server/sonar-auth-common/src/test/java/org/sonar/auth/OAuthRestClientTest.java
new file mode 100644
index 00000000000..7f1826e3e12
--- /dev/null
+++ b/server/sonar-auth-common/src/test/java/org/sonar/auth/OAuthRestClientTest.java
@@ -0,0 +1,127 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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.auth;
+
+import com.github.scribejava.core.builder.ServiceBuilder;
+import com.github.scribejava.core.builder.api.DefaultApi20;
+import com.github.scribejava.core.model.OAuth2AccessToken;
+import com.github.scribejava.core.model.Response;
+import com.github.scribejava.core.oauth.OAuth20Service;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.sonar.auth.OAuthRestClient.executePaginatedRequest;
+import static org.sonar.auth.OAuthRestClient.executeRequest;
+
+public class OAuthRestClientTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Rule
+ public MockWebServer mockWebServer = new MockWebServer();
+
+ private OAuth2AccessToken auth2AccessToken = mock(OAuth2AccessToken.class);
+
+ private String serverUrl;
+
+ private OAuth20Service oAuth20Service = new ServiceBuilder("API_KEY")
+ .apiSecret("API_SECRET")
+ .callback("CALLBACK")
+ .build(new TestAPI());
+
+ @Before
+ public void setUp() {
+ this.serverUrl = format("http://%s:%d", mockWebServer.getHostName(), mockWebServer.getPort());
+ }
+
+ @Test
+ public void execute_request() throws IOException {
+ String body = randomAlphanumeric(10);
+ mockWebServer.enqueue(new MockResponse().setBody(body));
+
+ Response response = executeRequest(serverUrl + "/test", oAuth20Service, auth2AccessToken);
+
+ assertThat(response.getBody()).isEqualTo(body);
+ }
+
+ @Test
+ public void fail_to_execute_request() throws IOException {
+ mockWebServer.enqueue(new MockResponse().setResponseCode(404).setBody("Error!"));
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage(format("Fail to execute request '%s/test'. HTTP code: 404, response: Error!", serverUrl));
+
+ executeRequest(serverUrl + "/test", oAuth20Service, auth2AccessToken);
+ }
+
+ @Test
+ public void execute_paginated_request() {
+ mockWebServer.enqueue(new MockResponse()
+ .setHeader("Link", "<" + serverUrl + "/test?per_page=100&page=2>; rel=\"next\", <" + serverUrl + "/test?per_page=100&page=2>; rel=\"last\"")
+ .setBody("A"));
+ mockWebServer.enqueue(new MockResponse()
+ .setHeader("Link", "<" + serverUrl + "/test?per_page=100&page=1>; rel=\"prev\", <" + serverUrl + "/test?per_page=100&page=1>; rel=\"first\"")
+ .setBody("B"));
+
+ List<String> response = executePaginatedRequest(serverUrl + "/test", oAuth20Service, auth2AccessToken, Arrays::asList);
+
+ assertThat(response).contains("A", "B");
+ }
+
+ @Test
+ public void fail_to_executed_paginated_request() {
+ mockWebServer.enqueue(new MockResponse()
+ .setHeader("Link", "<" + serverUrl + "/test?per_page=100&page=2>; rel=\"next\", <" + serverUrl + "/test?per_page=100&page=2>; rel=\"last\"")
+ .setBody("A"));
+ mockWebServer.enqueue(new MockResponse().setResponseCode(404).setBody("Error!"));
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage(format("Fail to execute request '%s/test?per_page=100&page=2'. HTTP code: 404, response: Error!", serverUrl));
+
+ executePaginatedRequest(serverUrl + "/test", oAuth20Service, auth2AccessToken, Arrays::asList);
+ }
+
+ private class TestAPI extends DefaultApi20 {
+
+ @Override
+ public String getAccessTokenEndpoint() {
+ return serverUrl + "/login/oauth/access_token";
+ }
+
+ @Override
+ protected String getAuthorizationBaseUrl() {
+ return serverUrl + "/login/oauth/authorize";
+ }
+
+ }
+}