aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-webserver-auth
diff options
context:
space:
mode:
authorAurelien Poscia <aurelien.poscia@sonarsource.com>2022-09-05 14:25:16 +0200
committersonartech <sonartech@sonarsource.com>2022-09-13 20:03:08 +0000
commit32fbddcc7a8607e56d2a1e0ab4831cd640e1e444 (patch)
tree53299e38d4ec7f532673828aa434768d1ba4c487 /server/sonar-webserver-auth
parent38ba648c3606e27dc0968bca56041fe7714f5662 (diff)
downloadsonarqube-32fbddcc7a8607e56d2a1e0ab4831cd640e1e444.tar.gz
sonarqube-32fbddcc7a8607e56d2a1e0ab4831cd640e1e444.zip
SONAR-17266 Implement authentification for GitHub webhook requests
Diffstat (limited to 'server/sonar-webserver-auth')
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/GithubWebhookAuthentication.java127
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/RequestAuthenticatorImpl.java1
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/user/GithubWebhookUserSession.java2
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/GithubWebhookAuthenticationTest.java154
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/user/GithubWebhookUserSessionTest.java5
-rw-r--r--server/sonar-webserver-auth/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker1
6 files changed, 270 insertions, 20 deletions
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/GithubWebhookAuthentication.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/GithubWebhookAuthentication.java
index f732d83b970..2fb47dd24d0 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/GithubWebhookAuthentication.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/GithubWebhookAuthentication.java
@@ -19,26 +19,143 @@
*/
package org.sonar.server.authentication;
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.security.MessageDigest;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
-import org.apache.commons.lang.StringUtils;
+import org.apache.commons.codec.digest.HmacAlgorithms;
+import org.apache.commons.codec.digest.HmacUtils;
+import org.sonar.api.config.internal.Encryption;
+import org.sonar.api.config.internal.Settings;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.alm.setting.ALM;
import org.sonar.server.authentication.event.AuthenticationEvent;
+import org.sonar.server.authentication.event.AuthenticationException;
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
+import static org.apache.commons.lang.StringUtils.isEmpty;
import static org.sonar.server.user.GithubWebhookUserSession.GITHUB_WEBHOOK_USER_NAME;
public class GithubWebhookAuthentication {
+ private static final Logger LOG = Loggers.get(GithubWebhookAuthentication.class);
+
+ @VisibleForTesting
+ static final String GITHUB_SIGNATURE_HEADER = "x-hub-signature-256";
+
+ @VisibleForTesting
+ static final String GITHUB_APP_ID_HEADER = "x-github-hook-installation-target-id";
+
+ @VisibleForTesting
+ static final String MSG_AUTHENTICATION_FAILED = "Failed to authenticate payload from Github webhook. Either the webhook was called by unexpected clients"
+ + " or the webhook secret set in SonarQube does not match the one from Github.";
+
+ @VisibleForTesting
+ static final String MSG_UNAUTHENTICATED_GITHUB_CALLS_DENIED = "Unauthenticated calls from GitHub are forbidden. A webhook secret must be defined in the GitHub App with Id %s.";
+
+ @VisibleForTesting
+ static final String MSG_NO_WEBHOOK_SECRET_FOUND = "Webhook secret for your GitHub app with Id %s must be set in SonarQube.";
+
+ @VisibleForTesting
+ static final String MSG_NO_BODY_FOUND = "No body found in GitHub Webhook event.";
+
+ private static final String SHA_256_PREFIX = "sha256=";
+
private final AuthenticationEvent authenticationEvent;
- public GithubWebhookAuthentication(AuthenticationEvent authenticationEvent) {
+ private final DbClient dbClient;
+
+ private final Encryption encryption;
+
+ public GithubWebhookAuthentication(AuthenticationEvent authenticationEvent, DbClient dbClient, Settings settings) {
this.authenticationEvent = authenticationEvent;
+ this.dbClient = dbClient;
+ this.encryption = settings.getEncryption();
}
public Optional<UserAuthResult> authenticate(HttpServletRequest request) {
- String authorizationHeader = request.getHeader("x-hub-signature-256");
- if (StringUtils.isEmpty(authorizationHeader)) {
+ String githubAppId = request.getHeader(GITHUB_APP_ID_HEADER);
+ if (isEmpty(githubAppId)) {
+ return Optional.empty();
+ }
+
+ String githubSignature = getGithubSignature(request, githubAppId);
+ String body = getBody(request);
+ String webhookSecret = getWebhookSecret(githubAppId);
+
+ String computedSignature = computeSignature(body, webhookSecret);
+ if (!checkEqualityTimingAttacksSafe(githubSignature, computedSignature)) {
+ logAuthenticationProblemAndThrow(MSG_AUTHENTICATION_FAILED);
+ }
+ return createAuthResult(request);
+ }
+
+ private static String getGithubSignature(HttpServletRequest request, String githubAppId) {
+ String githubSignature = request.getHeader(GITHUB_SIGNATURE_HEADER);
+ if (isEmpty(githubSignature) ) {
+ logAuthenticationProblemAndThrow(format(MSG_UNAUTHENTICATED_GITHUB_CALLS_DENIED, githubAppId));
+ }
+ return githubSignature;
+ }
+
+ private static String getBody(HttpServletRequest request) {
+ Optional<String> body = getBodyInternal(request);
+ if (body.isEmpty() || isEmpty(body.get())) {
+ logAuthenticationProblemAndThrow(MSG_NO_BODY_FOUND);
+ }
+ return body.get();
+ }
+
+
+ private static Optional<String> getBodyInternal(HttpServletRequest request) {
+ try {
+ String body = request.getReader().lines().collect(joining(System.lineSeparator()));
+ return Optional.of(body);
+ } catch (IOException e) {
+ LOG.debug("Unexpected error while trying to get the body of github webhook request", e);
return Optional.empty();
}
- //TODO SONAR-17269 implement authentication algorithm
+ }
+
+ private String getWebhookSecret(String githubAppId) {
+ String webhookSecret = fetchWebhookSecret(githubAppId);
+ if (isEmpty(webhookSecret)) {
+ logAuthenticationProblemAndThrow(format(MSG_NO_WEBHOOK_SECRET_FOUND, githubAppId));
+ }
+ return webhookSecret;
+ }
+
+ private String fetchWebhookSecret(String appId) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ return dbClient.almSettingDao().selectByAlmAndAppId(dbSession, ALM.GITHUB, appId)
+ .map(almSettingDto -> almSettingDto.getDecryptedWebhookSecret(encryption))
+ .orElse(null);
+ }
+ }
+
+ private static void logAuthenticationProblemAndThrow(String message) {
+ LOG.warn(message);
+ throw AuthenticationException.newBuilder()
+ .setSource(AuthenticationEvent.Source.githubWebhook())
+ .setMessage(message)
+ .build();
+ }
+
+ private static String computeSignature(String body, String webhookSecret) {
+ HmacUtils hmacUtils = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, webhookSecret);
+ return SHA_256_PREFIX + hmacUtils.hmacHex(body);
+ }
+
+ private static boolean checkEqualityTimingAttacksSafe(String githubSignature, String computedSignature) {
+ return MessageDigest.isEqual(githubSignature.getBytes(UTF_8), computedSignature.getBytes(UTF_8));
+ }
+
+ private Optional<UserAuthResult> createAuthResult(HttpServletRequest request) {
UserAuthResult userAuthResult = UserAuthResult.withGithubWebhook();
authenticationEvent.loginSuccess(request, GITHUB_WEBHOOK_USER_NAME, AuthenticationEvent.Source.githubWebhook());
return Optional.of(userAuthResult);
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/RequestAuthenticatorImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/RequestAuthenticatorImpl.java
index 23286abadea..1afd415ebb4 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/RequestAuthenticatorImpl.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/RequestAuthenticatorImpl.java
@@ -90,7 +90,6 @@ public class RequestAuthenticatorImpl implements RequestAuthenticator {
}
private UserAuthResult loadUser(HttpServletRequest request, HttpServletResponse response) {
-
Function<UserAuthResult.AuthType, Function<UserDto, UserAuthResult>> createUserAuthResult = type -> userDto -> new UserAuthResult(userDto, type);
// SSO authentication should come first in order to update JWT if user from header is not the same is user from JWT
return httpHeadersAuthentication.authenticate(request, response).map(createUserAuthResult.apply(SSO))
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/GithubWebhookUserSession.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/GithubWebhookUserSession.java
index e819d335f15..ac658860b2f 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/GithubWebhookUserSession.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/user/GithubWebhookUserSession.java
@@ -32,7 +32,7 @@ public class GithubWebhookUserSession extends AbstractUserSession {
@Override
public String getLogin() {
- throw new IllegalStateException("GithubWebhookUserSession does not contain a login.");
+ return GITHUB_WEBHOOK_USER_NAME;
}
@Override
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/GithubWebhookAuthenticationTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/GithubWebhookAuthenticationTest.java
index 667213caea5..96b48fee579 100644
--- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/GithubWebhookAuthenticationTest.java
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/GithubWebhookAuthenticationTest.java
@@ -19,32 +19,164 @@
*/
package org.sonar.server.authentication;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
+import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.Mockito;
+import org.sonar.api.config.internal.Encryption;
+import org.sonar.api.config.internal.Settings;
+import org.sonar.api.utils.log.LogAndArguments;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.db.DbTester;
+import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.server.authentication.event.AuthenticationEvent;
+import org.sonar.server.authentication.event.AuthenticationException;
+import org.springframework.lang.Nullable;
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNullElse;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.authentication.GithubWebhookAuthentication.GITHUB_APP_ID_HEADER;
+import static org.sonar.server.authentication.GithubWebhookAuthentication.GITHUB_SIGNATURE_HEADER;
+import static org.sonar.server.authentication.GithubWebhookAuthentication.MSG_AUTHENTICATION_FAILED;
+import static org.sonar.server.authentication.GithubWebhookAuthentication.MSG_NO_BODY_FOUND;
+import static org.sonar.server.authentication.GithubWebhookAuthentication.MSG_NO_WEBHOOK_SECRET_FOUND;
+import static org.sonar.server.authentication.GithubWebhookAuthentication.MSG_UNAUTHENTICATED_GITHUB_CALLS_DENIED;
-@RunWith(MockitoJUnitRunner.class)
public class GithubWebhookAuthenticationTest {
- @Mock
- private AuthenticationEvent authenticationEvent;
+ private static final String GITHUB_SIGNATURE = "sha256=1cd2d35d7bb5fc5738672bcdc959c4bc94ba308eb7008938a2f22b5713571925";
+ private static final String GITHUB_PAYLOAD = "{\"action\":\"closed_by_user\",\"alert\":{\"number\":2,\"created_at\":\"2022-08-16T13:02:17Z\",\"updated_at\":\"2022-09-05T08:35:05Z\",\"url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/code-scanning/alerts/2\",\"html_url\":\"https://github.com/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/security/code-scanning/2\",\"state\":\"dismissed\",\"fixed_at\":null,\"dismissed_by\":{\"login\":\"aurelien-poscia-sonarsource\",\"id\":100427063,\"node_id\":\"U_kgDOBfxlNw\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/100427063?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/aurelien-poscia-sonarsource\",\"html_url\":\"https://github.com/aurelien-poscia-sonarsource\",\"followers_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/followers\",\"following_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/subscriptions\",\"organizations_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/orgs\",\"repos_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/repos\",\"events_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/received_events\",\"type\":\"User\",\"site_admin\":false},\"dismissed_at\":\"2022-09-05T08:35:05Z\",\"dismissed_reason\":\"false positive\",\"dismissed_comment\":\"c'est pas un buuuuug\",\"rule\":{\"id\":\"js/inconsistent-use-of-new\",\"severity\":\"warning\",\"description\":\"Inconsistent use of 'new'\",\"name\":\"js/inconsistent-use-of-new\",\"tags\":[\"correctness\",\"language-features\",\"reliability\"],\"full_description\":\"If a function is intended to be a constructor, it should always be invoked with 'new'. Otherwise, it should always be invoked as a normal function, that is, without 'new'.\",\"help\":\"\",\"help_uri\":\"\"},\"tool\":{\"name\":\"CodeQL\",\"guid\":null,\"version\":\"2.0.0\"},\"most_recent_instance\":{\"ref\":\"refs/heads/main\",\"analysis_key\":\"(default)\",\"environment\":\"{}\",\"category\":\"\",\"state\":\"dismissed\",\"commit_sha\":\"b4f17b0f7612575b70e9708e0aeda7e8067e8404\",\"message\":{\"text\":\"Function resolvingPromise is sometimes invoked as a constructor (for example here), and sometimes as a normal function (for example here).\"},\"location\":{\"path\":\"src/promiseUtils.js\",\"start_line\":2,\"end_line\":2,\"start_column\":8,\"end_column\":37},\"classifications\":[]},\"instances_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/code-scanning/alerts/2/instances\"},\"ref\":\"\",\"commit_oid\":\"\",\"repository\":{\"id\":525365935,\"node_id\":\"R_kgDOH1Byrw\",\"name\":\"mmf-2821-github-scanning-alerts\",\"full_name\":\"aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts\",\"private\":false,\"owner\":{\"login\":\"aurelien-poscia-sonarsource\",\"id\":100427063,\"node_id\":\"U_kgDOBfxlNw\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/100427063?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/aurelien-poscia-sonarsource\",\"html_url\":\"https://github.com/aurelien-poscia-sonarsource\",\"followers_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/followers\",\"following_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/subscriptions\",\"organizations_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/orgs\",\"repos_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/repos\",\"events_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/received_events\",\"type\":\"User\",\"site_admin\":false},\"html_url\":\"https://github.com/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts\",\"description\":null,\"fork\":false,\"url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts\",\"forks_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/forks\",\"keys_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/keys{/key_id}\",\"collaborators_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/collaborators{/collaborator}\",\"teams_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/teams\",\"hooks_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/hooks\",\"issue_events_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/issues/events{/number}\",\"events_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/events\",\"assignees_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/assignees{/user}\",\"branches_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/branches{/branch}\",\"tags_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/tags\",\"blobs_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/git/blobs{/sha}\",\"git_tags_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/git/tags{/sha}\",\"git_refs_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/git/refs{/sha}\",\"trees_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/git/trees{/sha}\",\"statuses_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/statuses/{sha}\",\"languages_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/languages\",\"stargazers_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/stargazers\",\"contributors_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/contributors\",\"subscribers_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/subscribers\",\"subscription_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/subscription\",\"commits_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/commits{/sha}\",\"git_commits_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/git/commits{/sha}\",\"comments_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/comments{/number}\",\"issue_comment_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/issues/comments{/number}\",\"contents_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/contents/{+path}\",\"compare_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/compare/{base}...{head}\",\"merges_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/merges\",\"archive_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/{archive_format}{/ref}\",\"downloads_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/downloads\",\"issues_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/issues{/number}\",\"pulls_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/pulls{/number}\",\"milestones_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/milestones{/number}\",\"notifications_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/notifications{?since,all,participating}\",\"labels_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/labels{/name}\",\"releases_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/releases{/id}\",\"deployments_url\":\"https://api.github.com/repos/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts/deployments\",\"created_at\":\"2022-08-16T12:16:56Z\",\"updated_at\":\"2022-08-16T12:16:56Z\",\"pushed_at\":\"2022-08-18T10:08:48Z\",\"git_url\":\"git://github.com/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts.git\",\"ssh_url\":\"git@github.com:aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts.git\",\"clone_url\":\"https://github.com/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts.git\",\"svn_url\":\"https://github.com/aurelien-poscia-sonarsource/mmf-2821-github-scanning-alerts\",\"homepage\":null,\"size\":2,\"stargazers_count\":0,\"watchers_count\":0,\"language\":null,\"has_issues\":true,\"has_projects\":true,\"has_downloads\":true,\"has_wiki\":true,\"has_pages\":false,\"forks_count\":0,\"mirror_url\":null,\"archived\":false,\"disabled\":false,\"open_issues_count\":0,\"license\":null,\"allow_forking\":true,\"is_template\":false,\"web_commit_signoff_required\":false,\"topics\":[],\"visibility\":\"public\",\"forks\":0,\"open_issues\":0,\"watchers\":0,\"default_branch\":\"main\"},\"sender\":{\"login\":\"aurelien-poscia-sonarsource\",\"id\":100427063,\"node_id\":\"U_kgDOBfxlNw\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/100427063?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/aurelien-poscia-sonarsource\",\"html_url\":\"https://github.com/aurelien-poscia-sonarsource\",\"followers_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/followers\",\"following_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/subscriptions\",\"organizations_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/orgs\",\"repos_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/repos\",\"events_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/aurelien-poscia-sonarsource/received_events\",\"type\":\"User\",\"site_admin\":false},\"installation\":{\"id\":28299870,\"node_id\":\"MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjgyOTk4NzA=\"}}";
+ private static final String APP_ID = "APP_ID";
+ private static final String WEBHOOK_SECRET = "toto_secret";
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Rule
+ public final DbTester db = DbTester.create();
+
+ private final AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
+
+ private final Settings settings = mock(Settings.class);
+
+ private final Encryption encryption = mock(Encryption.class);
- @InjectMocks
private GithubWebhookAuthentication githubWebhookAuthentication;
+ private AlmSettingDto almSettingDto;
+
+ @Before
+ public void setUp() {
+ when(settings.getEncryption()).thenReturn(encryption);
+ githubWebhookAuthentication = new GithubWebhookAuthentication(authenticationEvent, db.getDbClient(), settings);
+ almSettingDto = db.almSettings().insertGitHubAlmSetting(
+ setting -> setting.setAppId(APP_ID),
+ setting -> setting.setWebhookSecret(WEBHOOK_SECRET));
+ }
+
+ @Test
+ public void authenticate_withComputedSignatureMatchingGithubSignature_returnsAuthentication() {
+ HttpServletRequest request = mockRequest(GITHUB_PAYLOAD, GITHUB_SIGNATURE);
+
+ Optional<UserAuthResult> authentication = githubWebhookAuthentication.authenticate(request);
+ assertThat(authentication).isPresent();
+
+ UserAuthResult userAuthResult = authentication.get();
+ assertThat(userAuthResult.getUserDto()).isNull();
+ assertThat(userAuthResult.getAuthType()).isEqualTo(UserAuthResult.AuthType.GITHUB_WEBHOOK);
+ assertThat(userAuthResult.getTokenDto()).isNull();
+
+ verify(authenticationEvent).loginSuccess(request, "github-webhook", AuthenticationEvent.Source.githubWebhook());
+ assertThat(logTester.getLogs()).isEmpty();
+ }
+
+ @Test
+ public void authenticate_withoutGithubSignatureHeader_throws() {
+ HttpServletRequest request = mockRequest(GITHUB_PAYLOAD, null);
+
+ String expectedMessage = format(MSG_UNAUTHENTICATED_GITHUB_CALLS_DENIED, APP_ID);
+ assertThatExceptionOfType(AuthenticationException.class)
+ .isThrownBy(() -> githubWebhookAuthentication.authenticate(request))
+ .withMessage(expectedMessage);
+ assertThat(logTester.getLogs(LoggerLevel.WARN)).extracting(LogAndArguments::getFormattedMsg).contains(expectedMessage);
+ }
+
@Test
- public void test(){
+ public void authenticate_withoutBody_throws() {
+ HttpServletRequest request = mockRequest(null, GITHUB_SIGNATURE);
- githubWebhookAuthentication.authenticate(mock(HttpServletRequest.class));
+ assertThatExceptionOfType(AuthenticationException.class)
+ .isThrownBy(() -> githubWebhookAuthentication.authenticate(request))
+ .withMessage(MSG_NO_BODY_FOUND);
+ assertThat(logTester.getLogs(LoggerLevel.WARN)).extracting(LogAndArguments::getFormattedMsg).contains(MSG_NO_BODY_FOUND);
+ }
- //TODO SONAR-17266
+ @Test
+ public void authenticate_withoutAppId_returnsEmpty() {
+ HttpServletRequest request = mockRequest(null, GITHUB_SIGNATURE);
+ when(request.getHeader(GITHUB_APP_ID_HEADER)).thenReturn(null);
+
+ assertThat(githubWebhookAuthentication.authenticate(request)).isEmpty();
+ assertThat(logTester.getLogs()).isEmpty();
+ }
+
+ @Test
+ public void authenticate_withWrongPayload_throws() {
+ HttpServletRequest request = mockRequest(GITHUB_PAYLOAD + "_", GITHUB_SIGNATURE);
+
+ assertThatExceptionOfType(AuthenticationException.class)
+ .isThrownBy(() -> githubWebhookAuthentication.authenticate(request))
+ .withMessage(MSG_AUTHENTICATION_FAILED);
+ assertThat(logTester.getLogs(LoggerLevel.WARN)).extracting(LogAndArguments::getFormattedMsg).contains(MSG_AUTHENTICATION_FAILED);
+ }
+
+ @Test
+ public void authenticate_withWrongSignature_throws() {
+ HttpServletRequest request = mockRequest(GITHUB_PAYLOAD, GITHUB_SIGNATURE + "_");
+
+ assertThatExceptionOfType(AuthenticationException.class)
+ .isThrownBy(() -> githubWebhookAuthentication.authenticate(request))
+ .withMessage(MSG_AUTHENTICATION_FAILED);
+ assertThat(logTester.getLogs(LoggerLevel.WARN)).extracting(LogAndArguments::getFormattedMsg).contains(MSG_AUTHENTICATION_FAILED);
+ }
+
+ @Test
+ public void authenticate_whenNoWebhookSecret_throws() {
+ HttpServletRequest request = mockRequest(GITHUB_PAYLOAD, GITHUB_SIGNATURE);
+ db.getDbClient().almSettingDao().update(db.getSession(), almSettingDto.setWebhookSecret(null), true);
+ db.commit();
+
+ String expectedMessage = format(MSG_NO_WEBHOOK_SECRET_FOUND, APP_ID);
+ assertThatExceptionOfType(AuthenticationException.class)
+ .isThrownBy(() -> githubWebhookAuthentication.authenticate(request))
+ .withMessage(expectedMessage);
+ assertThat(logTester.getLogs(LoggerLevel.WARN)).extracting(LogAndArguments::getFormattedMsg).contains(expectedMessage);
+ }
+ private static HttpServletRequest mockRequest(@Nullable String payload, @Nullable String gitHubSignature) {
+ HttpServletRequest request = mock(HttpServletRequest.class, Mockito.RETURNS_DEEP_STUBS);
+ try {
+ StringReader stringReader = new StringReader(requireNonNullElse(payload, ""));
+ BufferedReader bufferedReader = new BufferedReader(stringReader);
+ when(request.getReader()).thenReturn(bufferedReader);
+ when(request.getHeader(GITHUB_SIGNATURE_HEADER)).thenReturn(gitHubSignature);
+ when(request.getHeader(GITHUB_APP_ID_HEADER)).thenReturn(APP_ID);
+ } catch (IOException e) {
+ fail("mockRequest threw an exception: " + e.getMessage());
+ }
+ return request;
}
}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/GithubWebhookUserSessionTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/GithubWebhookUserSessionTest.java
index 26280fbf9b2..400b3f8971d 100644
--- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/GithubWebhookUserSessionTest.java
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/user/GithubWebhookUserSessionTest.java
@@ -25,6 +25,7 @@ import org.sonar.db.permission.GlobalPermission;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.sonar.server.user.GithubWebhookUserSession.GITHUB_WEBHOOK_USER_NAME;
public class GithubWebhookUserSessionTest {
@@ -32,7 +33,7 @@ public class GithubWebhookUserSessionTest {
@Test
public void getLogin() {
- assertThatIllegalStateException().isThrownBy(() -> githubWebhookUserSession.getLogin());
+ assertThat(githubWebhookUserSession.getLogin()).isEqualTo(GITHUB_WEBHOOK_USER_NAME);
}
@Test
@@ -42,7 +43,7 @@ public class GithubWebhookUserSessionTest {
@Test
public void getName() {
- assertThat(githubWebhookUserSession.getName()).isEqualTo("github-webhook");
+ assertThat(githubWebhookUserSession.getName()).isEqualTo(GITHUB_WEBHOOK_USER_NAME);
}
@Test
diff --git a/server/sonar-webserver-auth/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/server/sonar-webserver-auth/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 00000000000..1f0955d450f
--- /dev/null
+++ b/server/sonar-webserver-auth/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline