@@ -21,7 +21,7 @@ package org.sonar.alm.client; | |||
import java.io.IOException; | |||
import java.util.Optional; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonar.api.ce.ComputeEngineSide; | |||
import org.sonar.api.server.ServerSide; | |||
@@ -37,7 +37,7 @@ import okhttp3.ResponseBody; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonarqube.ws.client.OkHttpClientBuilder; | |||
import static com.google.common.base.Preconditions.checkArgument; |
@@ -26,7 +26,7 @@ import javax.annotation.Nullable; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.alm.client.ApplicationHttpClient.GetResponse; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import static java.lang.String.format; | |||
@@ -21,7 +21,7 @@ package org.sonar.alm.client; | |||
import java.util.List; | |||
import java.util.function.Function; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.auth.github.security.AccessToken; | |||
public interface PaginatedHttpClient { | |||
<E> List<E> get(String appUrl, AccessToken token, String query, Function<String, List<E>> responseDeserializer); |
@@ -39,20 +39,23 @@ import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.alm.client.ApplicationHttpClient; | |||
import org.sonar.alm.client.ApplicationHttpClient.GetResponse; | |||
import org.sonar.alm.client.github.GithubBinding.GsonGithubRepository; | |||
import org.sonar.alm.client.github.GithubBinding.GsonInstallations; | |||
import org.sonar.alm.client.github.GithubBinding.GsonRepositorySearch; | |||
import org.sonar.alm.client.github.api.GsonRepositoryCollaborator; | |||
import org.sonar.alm.client.github.api.GsonRepositoryTeam; | |||
import org.sonar.alm.client.github.config.GithubAppConfiguration; | |||
import org.sonar.alm.client.github.config.GithubAppInstallation; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.auth.github.AppInstallationToken; | |||
import org.sonar.auth.github.GithubBinding; | |||
import org.sonar.auth.github.GithubBinding.GsonGithubRepository; | |||
import org.sonar.auth.github.GithubBinding.GsonInstallations; | |||
import org.sonar.auth.github.GithubBinding.GsonRepositorySearch; | |||
import org.sonar.auth.github.GsonRepositoryCollaborator; | |||
import org.sonar.auth.github.GsonRepositoryTeam; | |||
import org.sonar.auth.github.GithubAppConfiguration; | |||
import org.sonar.auth.github.GithubAppInstallation; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonar.alm.client.github.security.AppToken; | |||
import org.sonar.alm.client.github.security.GithubAppSecurity; | |||
import org.sonar.alm.client.github.security.UserAccessToken; | |||
import org.sonar.auth.github.security.UserAccessToken; | |||
import org.sonar.alm.client.gitlab.GsonApp; | |||
import org.sonar.api.internal.apachecommons.lang.StringUtils; | |||
import org.sonar.auth.github.GitHubSettings; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.server.exceptions.ServerException; | |||
import org.sonarqube.ws.client.HttpException; | |||
@@ -193,11 +196,11 @@ public class GithubApplicationClientImpl implements GithubApplicationClient { | |||
return organizations; | |||
} | |||
organizations.setTotal(gsonInstallations.get().totalCount); | |||
if (gsonInstallations.get().installations != null) { | |||
organizations.setOrganizations(gsonInstallations.get().installations.stream() | |||
.map(gsonInstallation -> new Organization(gsonInstallation.account.id, gsonInstallation.account.login, null, null, null, null, null, | |||
gsonInstallation.targetType)) | |||
organizations.setTotal(gsonInstallations.get().getTotalCount()); | |||
if (gsonInstallations.get().getInstallations() != null) { | |||
organizations.setOrganizations(gsonInstallations.get().getInstallations().stream() | |||
.map(gsonInstallation -> new Organization(gsonInstallation.getAccount().getId(), gsonInstallation.getAccount().getLogin(), null, null, null, null, null, | |||
gsonInstallation.getTargetType())) | |||
.toList()); | |||
} | |||
@@ -267,10 +270,10 @@ public class GithubApplicationClientImpl implements GithubApplicationClient { | |||
return repositories; | |||
} | |||
repositories.setTotal(gsonRepositories.get().totalCount); | |||
repositories.setTotal(gsonRepositories.get().getTotalCount()); | |||
if (gsonRepositories.get().items != null) { | |||
repositories.setRepositories(gsonRepositories.get().items.stream() | |||
if (gsonRepositories.get().getItems() != null) { | |||
repositories.setRepositories(gsonRepositories.get().getItems().stream() | |||
.map(GsonGithubRepository::toRepository) | |||
.toList()); | |||
} |
@@ -20,10 +20,11 @@ | |||
package org.sonar.alm.client.github; | |||
import java.util.Optional; | |||
import org.sonar.alm.client.github.config.GithubAppConfiguration; | |||
import org.sonar.auth.github.GithubAppConfiguration; | |||
import org.sonar.api.config.internal.Encryption; | |||
import org.sonar.api.config.internal.Settings; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import static org.apache.commons.lang.StringUtils.isBlank; |
@@ -22,15 +22,17 @@ package org.sonar.alm.client.github.config; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubBinding.Permissions; | |||
import org.sonar.auth.github.GithubAppConfiguration; | |||
import org.sonar.auth.github.GithubAppInstallation; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.auth.github.GithubBinding.Permissions; | |||
import org.sonar.api.ce.ComputeEngineSide; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.auth.github.GitHubSettings; | |||
import org.sonarqube.ws.client.HttpException; | |||
import static java.lang.Long.parseLong; | |||
import static org.sonar.alm.client.github.GithubBinding.GsonApp; | |||
import static org.sonar.auth.github.GithubBinding.GsonApp; | |||
import static org.sonar.alm.client.github.config.ConfigCheckResult.ApplicationStatus; | |||
import static org.sonar.alm.client.github.config.ConfigCheckResult.ConfigStatus; | |||
import static org.sonar.alm.client.github.config.ConfigCheckResult.InstallationStatus; |
@@ -20,6 +20,7 @@ | |||
package org.sonar.alm.client.github.security; | |||
import javax.annotation.concurrent.Immutable; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import static java.util.Objects.requireNonNull; | |||
@@ -20,7 +20,7 @@ | |||
package org.sonar.alm.client.gitlab; | |||
import java.util.Objects; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.auth.github.security.AccessToken; | |||
public class GitlabToken implements AccessToken { | |||
private final String token; |
@@ -33,7 +33,7 @@ import org.mockito.InjectMocks; | |||
import org.mockito.Mock; | |||
import org.mockito.junit.MockitoJUnitRunner; | |||
import org.slf4j.event.Level; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonar.api.testfixtures.log.LogTester; | |||
import static org.assertj.core.api.Assertions.assertThat; |
@@ -41,8 +41,8 @@ import org.sonar.alm.client.GenericApplicationHttpClient; | |||
import org.sonar.alm.client.TimeoutConfiguration; | |||
import org.sonar.alm.client.ApplicationHttpClient.GetResponse; | |||
import org.sonar.alm.client.ApplicationHttpClient.Response; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.alm.client.github.security.UserAccessToken; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonar.auth.github.security.UserAccessToken; | |||
import org.sonar.api.testfixtures.log.LogTester; | |||
import org.sonar.api.utils.log.LoggerLevel; | |||
@@ -37,19 +37,22 @@ import org.junit.runner.RunWith; | |||
import org.mockito.ArgumentCaptor; | |||
import org.slf4j.event.Level; | |||
import org.sonar.alm.client.ApplicationHttpClient.RateLimit; | |||
import org.sonar.alm.client.github.api.GsonRepositoryCollaborator; | |||
import org.sonar.alm.client.github.api.GsonRepositoryTeam; | |||
import org.sonar.alm.client.github.config.GithubAppConfiguration; | |||
import org.sonar.alm.client.github.config.GithubAppInstallation; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.auth.github.GsonRepositoryCollaborator; | |||
import org.sonar.auth.github.GsonRepositoryTeam; | |||
import org.sonar.auth.github.AppInstallationToken; | |||
import org.sonar.auth.github.GithubAppConfiguration; | |||
import org.sonar.auth.github.GithubAppInstallation; | |||
import org.sonar.auth.github.GithubBinding; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonar.alm.client.github.security.AppToken; | |||
import org.sonar.alm.client.github.security.GithubAppSecurity; | |||
import org.sonar.alm.client.github.security.UserAccessToken; | |||
import org.sonar.auth.github.security.UserAccessToken; | |||
import org.sonar.api.testfixtures.log.LogAndArguments; | |||
import org.sonar.api.testfixtures.log.LogTester; | |||
import org.sonar.api.utils.log.LoggerLevel; | |||
import org.sonar.auth.github.GitHubSettings; | |||
import org.sonar.auth.github.GsonRepositoryPermissions; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonarqube.ws.client.HttpException; | |||
import static java.net.HttpURLConnection.HTTP_CREATED; |
@@ -23,7 +23,7 @@ import javax.annotation.Nullable; | |||
import org.junit.BeforeClass; | |||
import org.junit.Test; | |||
import org.mockito.ArgumentCaptor; | |||
import org.sonar.alm.client.github.config.GithubAppConfiguration; | |||
import org.sonar.auth.github.GithubAppConfiguration; | |||
import org.sonar.api.config.internal.Encryption; | |||
import org.sonar.api.config.internal.Settings; | |||
import org.sonar.db.alm.setting.ALM; |
@@ -27,9 +27,11 @@ import org.mockito.ArgumentCaptor; | |||
import org.mockito.InjectMocks; | |||
import org.mockito.Mock; | |||
import org.mockito.junit.MockitoJUnitRunner; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubBinding.GsonApp; | |||
import org.sonar.alm.client.github.GithubBinding.Permissions; | |||
import org.sonar.auth.github.GithubAppConfiguration; | |||
import org.sonar.auth.github.GithubAppInstallation; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.auth.github.GithubBinding.GsonApp; | |||
import org.sonar.auth.github.GithubBinding.Permissions; | |||
import org.sonar.auth.github.GitHubSettings; | |||
import org.sonarqube.ws.client.HttpException; | |||
@@ -28,7 +28,7 @@ import java.time.ZoneId; | |||
import java.util.Random; | |||
import org.junit.Test; | |||
import org.junit.runner.RunWith; | |||
import org.sonar.alm.client.github.config.GithubAppConfiguration; | |||
import org.sonar.auth.github.GithubAppConfiguration; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; | |||
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; |
@@ -20,6 +20,7 @@ dependencies { | |||
testImplementation 'com.squareup.okhttp3:mockwebserver' | |||
testImplementation 'com.squareup.okhttp3:okhttp' | |||
testImplementation 'com.tngtech.java:junit-dataprovider' | |||
testImplementation 'junit:junit' | |||
testImplementation 'org.assertj:assertj-core' | |||
testImplementation 'org.mockito:mockito-core' |
@@ -17,10 +17,10 @@ | |||
* 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.alm.client.github; | |||
package org.sonar.auth.github; | |||
import javax.annotation.concurrent.Immutable; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import static java.util.Objects.requireNonNull; | |||
@@ -23,15 +23,21 @@ import com.github.scribejava.core.builder.ServiceBuilder; | |||
import com.github.scribejava.core.model.OAuth2AccessToken; | |||
import com.github.scribejava.core.oauth.OAuth20Service; | |||
import java.io.IOException; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import java.util.concurrent.ExecutionException; | |||
import org.sonar.api.server.authentication.Display; | |||
import org.sonar.api.server.authentication.OAuth2IdentityProvider; | |||
import org.sonar.api.server.authentication.UnauthorizedException; | |||
import org.sonar.api.server.authentication.UserIdentity; | |||
import org.sonar.api.server.http.HttpRequest; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.auth.github.scribe.ScribeServiceBuilder; | |||
import static com.google.common.base.Preconditions.checkState; | |||
import static java.lang.Long.parseLong; | |||
import static java.lang.String.format; | |||
import static org.sonar.auth.github.GitHubSettings.DEFAULT_API_URL; | |||
public class GitHubIdentityProvider implements OAuth2IdentityProvider { | |||
@@ -41,12 +47,17 @@ public class GitHubIdentityProvider implements OAuth2IdentityProvider { | |||
private final UserIdentityFactory userIdentityFactory; | |||
private final ScribeGitHubApi scribeApi; | |||
private final GitHubRestClient gitHubRestClient; | |||
private final GithubApplicationClient githubAppClient; | |||
private final ScribeServiceBuilder scribeServiceBuilder; | |||
public GitHubIdentityProvider(GitHubSettings settings, UserIdentityFactory userIdentityFactory, ScribeGitHubApi scribeApi, GitHubRestClient gitHubRestClient) { | |||
public GitHubIdentityProvider(GitHubSettings settings, UserIdentityFactory userIdentityFactory, ScribeGitHubApi scribeApi, GitHubRestClient gitHubRestClient, | |||
GithubApplicationClient githubAppClient, ScribeServiceBuilder scribeServiceBuilder) { | |||
this.settings = settings; | |||
this.userIdentityFactory = userIdentityFactory; | |||
this.scribeApi = scribeApi; | |||
this.gitHubRestClient = gitHubRestClient; | |||
this.githubAppClient = githubAppClient; | |||
this.scribeServiceBuilder = scribeServiceBuilder; | |||
} | |||
@Override | |||
@@ -107,7 +118,8 @@ public class GitHubIdentityProvider implements OAuth2IdentityProvider { | |||
context.verifyCsrfState(); | |||
HttpRequest request = context.getHttpRequest(); | |||
OAuth20Service scribe = newScribeBuilder(context).build(scribeApi); | |||
OAuth20Service scribe = scribeServiceBuilder.buildScribeService(settings.clientId(), settings.clientSecret(), context.getCallbackUrl(), scribeApi); | |||
String code = request.getParameter("code"); | |||
OAuth2AccessToken accessToken = scribe.getAccessToken(code); | |||
@@ -128,18 +140,25 @@ public class GitHubIdentityProvider implements OAuth2IdentityProvider { | |||
context.redirectToRequestedPage(); | |||
} | |||
boolean isOrganizationMembershipRequired() { | |||
return !settings.getOrganizations().isEmpty(); | |||
private void check(OAuth20Service scribe, OAuth2AccessToken accessToken, GsonUser user) throws InterruptedException, ExecutionException, IOException { | |||
if (!isUserAuthorized(scribe, accessToken, user.getLogin())) { | |||
String message = settings.getOrganizations().isEmpty() | |||
? format("'%s' must be a member of at least one organization which has installed the SonarQube GitHub app", user.getLogin()) | |||
: format("'%s' must be a member of at least one organization: '%s'", user.getLogin(), String.join("', '", settings.getOrganizations().stream().sorted().toList())); | |||
throw new UnauthorizedException(message); | |||
} | |||
} | |||
private void check(OAuth20Service scribe, OAuth2AccessToken accessToken, GsonUser user) throws InterruptedException, ExecutionException, IOException { | |||
if (isUnauthorized(scribe, accessToken, user.getLogin())) { | |||
throw new UnauthorizedException(format("'%s' must be a member of at least one organization: '%s'", user.getLogin(), String.join("', '", settings.getOrganizations()))); | |||
private boolean isUserAuthorized(OAuth20Service scribe, OAuth2AccessToken accessToken, String login) throws IOException, ExecutionException, InterruptedException { | |||
if (isOrganizationMembershipRequired()) { | |||
return isOrganizationsMember(scribe, accessToken, login); | |||
} else { | |||
return isMemberOfInstallationOrganization(scribe, accessToken, login); | |||
} | |||
} | |||
private boolean isUnauthorized(OAuth20Service scribe, OAuth2AccessToken accessToken, String login) throws IOException, ExecutionException, InterruptedException { | |||
return isOrganizationMembershipRequired() && !isOrganizationsMember(scribe, accessToken, login); | |||
private boolean isOrganizationMembershipRequired() { | |||
return !settings.getOrganizations().isEmpty(); | |||
} | |||
private boolean isOrganizationsMember(OAuth20Service scribe, OAuth2AccessToken accessToken, String login) throws IOException, ExecutionException, InterruptedException { | |||
@@ -151,6 +170,27 @@ public class GitHubIdentityProvider implements OAuth2IdentityProvider { | |||
return false; | |||
} | |||
private boolean isMemberOfInstallationOrganization(OAuth20Service scribe, OAuth2AccessToken accessToken, String login) | |||
throws IOException, ExecutionException, InterruptedException { | |||
GithubAppConfiguration githubAppConfiguration = githubAppConfiguration(); | |||
List<GithubAppInstallation> githubAppInstallations = githubAppClient.getWhitelistedGithubAppInstallations(githubAppConfiguration); | |||
for (GithubAppInstallation installation : githubAppInstallations) { | |||
if (gitHubRestClient.isOrganizationMember(scribe, accessToken, installation.organizationName(), login)) { | |||
return true; | |||
} | |||
} | |||
return false; | |||
} | |||
private GithubAppConfiguration githubAppConfiguration() { | |||
String apiEndpoint = Optional.ofNullable(settings.apiURL()).orElse(DEFAULT_API_URL); | |||
try { | |||
return new GithubAppConfiguration(parseLong(settings.appId()), settings.privateKey(), apiEndpoint); | |||
} catch (NumberFormatException numberFormatException) { | |||
throw new IllegalStateException("Github configuration is not complete. Please check your configuration under the Authentication > GitHub tab"); | |||
} | |||
} | |||
private ServiceBuilder newScribeBuilder(OAuth2IdentityProvider.OAuth2Context context) { | |||
checkState(isEnabled(), "GitHub authentication is disabled"); | |||
return new ServiceBuilder(settings.clientId()) |
@@ -21,6 +21,7 @@ package org.sonar.auth.github; | |||
import java.util.List; | |||
import org.sonar.api.config.PropertyDefinition; | |||
import org.sonar.auth.github.scribe.ScribeServiceBuilder; | |||
import org.sonar.core.platform.Module; | |||
import static org.sonar.auth.github.GitHubSettings.definitions; | |||
@@ -30,8 +31,8 @@ public class GitHubModule extends Module { | |||
GitHubIdentityProvider.class, | |||
GitHubRestClient.class, | |||
UserIdentityFactoryImpl.class, | |||
ScribeGitHubApi.class | |||
); | |||
ScribeGitHubApi.class, | |||
ScribeServiceBuilder.class); | |||
@Override | |||
protected void configureModule() { |
@@ -45,13 +45,13 @@ public class GitHubRestClient { | |||
this.settings = settings; | |||
} | |||
GsonUser getUser(OAuth20Service scribe, OAuth2AccessToken accessToken) throws IOException { | |||
public GsonUser getUser(OAuth20Service scribe, OAuth2AccessToken accessToken) throws IOException { | |||
String responseBody = executeRequest(settings.apiURL() + "user", scribe, accessToken).getBody(); | |||
LOGGER.trace("User response received : {}", responseBody); | |||
return GsonUser.parse(responseBody); | |||
} | |||
String getEmail(OAuth20Service scribe, OAuth2AccessToken accessToken) throws IOException { | |||
public String getEmail(OAuth20Service scribe, OAuth2AccessToken accessToken) throws IOException { | |||
String responseBody = executeRequest(settings.apiURL() + "user/emails", scribe, accessToken).getBody(); | |||
LOGGER.trace("Emails response received : {}", responseBody); | |||
List<GsonEmail> emails = GsonEmail.parse(responseBody); | |||
@@ -62,7 +62,7 @@ public class GitHubRestClient { | |||
.orElse(null); | |||
} | |||
List<GsonTeam> getTeams(OAuth20Service scribe, OAuth2AccessToken accessToken) { | |||
public List<GsonTeam> getTeams(OAuth20Service scribe, OAuth2AccessToken accessToken) { | |||
return executePaginatedRequest(settings.apiURL() + "user/teams", scribe, accessToken, GsonTeam::parse); | |||
} | |||
@@ -74,7 +74,7 @@ public class GitHubRestClient { | |||
* | |||
* @see <a href="https://developer.github.com/v3/orgs/members/#response-if-requester-is-an-organization-member-and-user-is-a-member">GitHub members API</a> | |||
*/ | |||
boolean isOrganizationMember(OAuth20Service scribe, OAuth2AccessToken accessToken, String organization, String login) | |||
public boolean isOrganizationMember(OAuth20Service scribe, OAuth2AccessToken accessToken, String organization, String login) | |||
throws IOException, ExecutionException, InterruptedException { | |||
String requestUrl = settings.apiURL() + format("orgs/%s/members/%s", organization, login); | |||
OAuthRequest request = new OAuthRequest(Verb.GET, requestUrl); |
@@ -81,11 +81,11 @@ public class GitHubSettings implements DevOpsPlatformSettings { | |||
this.dbClient = dbClient; | |||
} | |||
String clientId() { | |||
public String clientId() { | |||
return configuration.get(CLIENT_ID).orElse(""); | |||
} | |||
String clientSecret() { | |||
public String clientSecret() { | |||
return configuration.get(CLIENT_SECRET).orElse(""); | |||
} | |||
@@ -101,11 +101,11 @@ public class GitHubSettings implements DevOpsPlatformSettings { | |||
return configuration.getBoolean(ENABLED).orElse(false) && !clientId().isEmpty() && !clientSecret().isEmpty(); | |||
} | |||
boolean allowUsersToSignUp() { | |||
public boolean allowUsersToSignUp() { | |||
return configuration.getBoolean(ALLOW_USERS_TO_SIGN_UP).orElse(false); | |||
} | |||
boolean syncGroups() { | |||
public boolean syncGroups() { | |||
return configuration.getBoolean(GROUPS_SYNC).orElse(false); | |||
} | |||
@@ -17,7 +17,7 @@ | |||
* 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.alm.client.github.config; | |||
package org.sonar.auth.github; | |||
import com.google.common.base.MoreObjects; | |||
import java.util.regex.Pattern; |
@@ -17,8 +17,6 @@ | |||
* 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.alm.client.github.config; | |||
import org.sonar.alm.client.github.GithubBinding; | |||
package org.sonar.auth.github; | |||
public record GithubAppInstallation(String installationId, String organizationName, GithubBinding.Permissions permissions, boolean isSuspended) {} |
@@ -17,14 +17,14 @@ | |||
* 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.alm.client.github; | |||
package org.sonar.auth.github; | |||
import com.google.gson.annotations.SerializedName; | |||
import java.util.List; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import static org.sonar.alm.client.github.GithubApplicationClient.Repository; | |||
import static org.sonar.auth.github.client.GithubApplicationClient.Repository; | |||
public class GithubBinding { | |||
@@ -71,6 +71,14 @@ public class GithubBinding { | |||
// recommended: | |||
// http://stackoverflow.com/a/18645370/229031 | |||
} | |||
public int getTotalCount() { | |||
return totalCount; | |||
} | |||
public List<GsonInstallation> getInstallations() { | |||
return installations; | |||
} | |||
} | |||
public static class GsonInstallation { | |||
@@ -133,6 +141,10 @@ public class GithubBinding { | |||
// http://stackoverflow.com/a/18645370/229031 | |||
} | |||
public long getId() { | |||
return id; | |||
} | |||
public String getLogin() { | |||
return login; | |||
} | |||
@@ -282,6 +294,14 @@ public class GithubBinding { | |||
// recommended: | |||
// http://stackoverflow.com/a/18645370/229031 | |||
} | |||
public int getTotalCount() { | |||
return totalCount; | |||
} | |||
public List<GsonGithubRepository> getItems() { | |||
return items; | |||
} | |||
} | |||
public static class GsonGithubRepository { |
@@ -17,7 +17,7 @@ | |||
* 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.alm.client.github.api; | |||
package org.sonar.auth.github; | |||
import com.google.gson.annotations.SerializedName; | |||
import org.sonar.auth.github.GsonRepositoryPermissions; |
@@ -17,7 +17,7 @@ | |||
* 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.alm.client.github.api; | |||
package org.sonar.auth.github; | |||
import com.google.gson.annotations.SerializedName; | |||
import org.sonar.auth.github.GsonRepositoryPermissions; |
@@ -17,7 +17,7 @@ | |||
* 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.alm.client.github; | |||
package org.sonar.auth.github.client; | |||
import com.google.gson.annotations.SerializedName; | |||
import java.util.List; | |||
@@ -26,13 +26,15 @@ import java.util.Set; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import javax.annotation.concurrent.Immutable; | |||
import org.sonar.alm.client.github.api.GsonRepositoryCollaborator; | |||
import org.sonar.alm.client.github.api.GsonRepositoryTeam; | |||
import org.sonar.alm.client.github.config.GithubAppConfiguration; | |||
import org.sonar.alm.client.github.config.GithubAppInstallation; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.alm.client.github.security.UserAccessToken; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.auth.github.AppInstallationToken; | |||
import org.sonar.auth.github.GithubAppConfiguration; | |||
import org.sonar.auth.github.GithubAppInstallation; | |||
import org.sonar.auth.github.GithubBinding; | |||
import org.sonar.auth.github.GsonRepositoryCollaborator; | |||
import org.sonar.auth.github.GsonRepositoryTeam; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonar.auth.github.security.UserAccessToken; | |||
@ServerSide | |||
public interface GithubApplicationClient { | |||
@@ -105,7 +107,7 @@ public interface GithubApplicationClient { | |||
private List<Repository> repositories; | |||
public Repositories() { | |||
//nothing to do | |||
// nothing to do | |||
} | |||
public int getTotal() { | |||
@@ -204,7 +206,7 @@ public interface GithubApplicationClient { | |||
private List<Organization> organizations; | |||
public Organizations() { | |||
//nothing to do | |||
// nothing to do | |||
} | |||
public int getTotal() { |
@@ -18,6 +18,6 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.alm.client.github.api; | |||
package org.sonar.auth.github.client; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -0,0 +1,35 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 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.github.scribe; | |||
import com.github.scribejava.core.builder.ServiceBuilder; | |||
import com.github.scribejava.core.oauth.OAuth20Service; | |||
import org.sonar.auth.github.ScribeGitHubApi; | |||
public class ScribeServiceBuilder { | |||
public OAuth20Service buildScribeService(String clientId, String clientSecret, String callbackUrl, ScribeGitHubApi scribeApi) { | |||
return new ServiceBuilder(clientId) | |||
.apiSecret(clientSecret) | |||
.callback(callbackUrl) | |||
.build(scribeApi); | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 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.github.scribe; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -17,7 +17,7 @@ | |||
* 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.alm.client.github.security; | |||
package org.sonar.auth.github.security; | |||
/** | |||
* Token used to authenticate requests to Github API |
@@ -17,7 +17,7 @@ | |||
* 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.alm.client.github.security; | |||
package org.sonar.auth.github.security; | |||
public class UserAccessToken implements AccessToken { | |||
@@ -0,0 +1,23 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 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.github.security; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -17,7 +17,7 @@ | |||
* 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.alm.client.github; | |||
package org.sonar.auth.github; | |||
import org.junit.Test; | |||
@@ -19,28 +19,49 @@ | |||
*/ | |||
package org.sonar.auth.github; | |||
import com.github.scribejava.core.model.OAuth2AccessToken; | |||
import com.github.scribejava.core.oauth.OAuth20Service; | |||
import java.io.IOException; | |||
import java.util.List; | |||
import java.util.concurrent.ExecutionException; | |||
import org.junit.Test; | |||
import org.sonar.api.config.internal.MapSettings; | |||
import org.sonar.api.server.authentication.OAuth2IdentityProvider; | |||
import org.sonar.api.server.authentication.UnauthorizedException; | |||
import org.sonar.api.server.authentication.UserIdentity; | |||
import org.sonar.api.server.http.HttpRequest; | |||
import org.sonar.auth.github.GithubBinding.Permissions; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.auth.github.scribe.ScribeServiceBuilder; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.server.property.InternalProperties; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.verify; | |||
import static org.mockito.Mockito.when; | |||
import static org.sonar.api.server.authentication.OAuth2IdentityProvider.CallbackContext; | |||
import static org.sonar.api.server.authentication.OAuth2IdentityProvider.InitContext; | |||
import static org.sonar.auth.github.GitHubSettings.APP_ID; | |||
import static org.sonar.auth.github.GitHubSettings.CLIENT_ID; | |||
import static org.sonar.auth.github.GitHubSettings.CLIENT_SECRET; | |||
import static org.sonar.auth.github.GitHubSettings.ENABLED; | |||
import static org.sonar.auth.github.GitHubSettings.ORGANIZATIONS; | |||
import static org.sonar.auth.github.GitHubSettings.PRIVATE_KEY; | |||
public class GitHubIdentityProviderTest { | |||
private MapSettings settings = new MapSettings(); | |||
private InternalProperties internalProperties = mock(InternalProperties.class); | |||
private GitHubSettings gitHubSettings = new GitHubSettings(settings.asConfig(), internalProperties, mock(DbClient.class)); | |||
private UserIdentityFactoryImpl userIdentityFactory = mock(UserIdentityFactoryImpl.class); | |||
private ScribeGitHubApi scribeApi = new ScribeGitHubApi(gitHubSettings); | |||
private GitHubRestClient gitHubRestClient = new GitHubRestClient(gitHubSettings); | |||
private GitHubIdentityProvider underTest = new GitHubIdentityProvider(gitHubSettings, userIdentityFactory, scribeApi, gitHubRestClient); | |||
private GitHubRestClient gitHubRestClient = mock(); | |||
private GithubApplicationClient githubAppClient = mock(); | |||
private ScribeServiceBuilder scribeServiceBuilder = mock(); | |||
private GitHubIdentityProvider underTest = new GitHubIdentityProvider(gitHubSettings, userIdentityFactory, scribeApi, gitHubRestClient, githubAppClient, scribeServiceBuilder); | |||
@Test | |||
public void check_fields() { | |||
@@ -72,7 +93,7 @@ public class GitHubIdentityProviderTest { | |||
@Test | |||
public void init() { | |||
setSettings(true); | |||
OAuth2IdentityProvider.InitContext context = mock(OAuth2IdentityProvider.InitContext.class); | |||
InitContext context = mock(InitContext.class); | |||
when(context.generateCsrfState()).thenReturn("state"); | |||
when(context.getCallbackUrl()).thenReturn("http://localhost/callback"); | |||
settings.setProperty("sonar.auth.github.webUrl", "https://github.com/"); | |||
@@ -91,7 +112,7 @@ public class GitHubIdentityProviderTest { | |||
setSettings(true); | |||
settings.setProperty("sonar.auth.github.groupsSync", "true"); | |||
settings.setProperty("sonar.auth.github.webUrl", "https://github.com/"); | |||
OAuth2IdentityProvider.InitContext context = mock(OAuth2IdentityProvider.InitContext.class); | |||
InitContext context = mock(InitContext.class); | |||
when(context.generateCsrfState()).thenReturn("state"); | |||
when(context.getCallbackUrl()).thenReturn("http://localhost/callback"); | |||
@@ -109,7 +130,7 @@ public class GitHubIdentityProviderTest { | |||
setSettings(true); | |||
settings.setProperty("sonar.auth.github.organizations", "example"); | |||
settings.setProperty("sonar.auth.github.webUrl", "https://github.com/"); | |||
OAuth2IdentityProvider.InitContext context = mock(OAuth2IdentityProvider.InitContext.class); | |||
InitContext context = mock(InitContext.class); | |||
when(context.generateCsrfState()).thenReturn("state"); | |||
when(context.getCallbackUrl()).thenReturn("http://localhost/callback"); | |||
@@ -126,7 +147,7 @@ public class GitHubIdentityProviderTest { | |||
@Test | |||
public void fail_to_init_when_disabled() { | |||
setSettings(false); | |||
OAuth2IdentityProvider.InitContext context = mock(OAuth2IdentityProvider.InitContext.class); | |||
InitContext context = mock(InitContext.class); | |||
assertThatThrownBy(() -> underTest.init(context)) | |||
.isInstanceOf(IllegalStateException.class) | |||
@@ -155,28 +176,117 @@ public class GitHubIdentityProviderTest { | |||
} | |||
@Test | |||
public void organization_membership_required() { | |||
setSettings(true); | |||
settings.setProperty("sonar.auth.github.organizations", "example"); | |||
assertThat(underTest.isOrganizationMembershipRequired()).isTrue(); | |||
settings.setProperty("sonar.auth.github.organizations", "example0, example1"); | |||
assertThat(underTest.isOrganizationMembershipRequired()).isTrue(); | |||
public void callback_whenOrganizationsAreDefinedAndUserBelongsToOne_shouldAuthenticateAndRedirect() throws IOException, ExecutionException, InterruptedException { | |||
UserIdentity userIdentity = mock(UserIdentity.class); | |||
CallbackContext context = mockUserBelongingToOrganization(userIdentity); | |||
settings.setProperty(ORGANIZATIONS, "organization1,organization2"); | |||
underTest.callback(context); | |||
verify(context).authenticate(userIdentity); | |||
verify(context).redirectToRequestedPage(); | |||
} | |||
@Test | |||
public void organization_membership_not_required() { | |||
public void callback_whenOrganizationsAreDefinedAndDoesntBelongToOne_shouldThrow() throws IOException, ExecutionException, InterruptedException { | |||
UserIdentity userIdentity = mock(UserIdentity.class); | |||
CallbackContext context = mockUserNotBelongingToOrganization(userIdentity); | |||
settings.setProperty(ORGANIZATIONS, "organization1,organization2"); | |||
assertThatThrownBy(() -> underTest.callback(context)) | |||
.isInstanceOf(UnauthorizedException.class) | |||
.hasMessage("'login' must be a member of at least one organization: 'organization1', 'organization2'"); | |||
} | |||
@Test | |||
public void callback_whenOrganizationsAreNotDefinedAndUserBelongsToInstallationOrganization_shouldAuthenticateAndRedirect() | |||
throws IOException, ExecutionException, InterruptedException { | |||
UserIdentity userIdentity = mock(UserIdentity.class); | |||
CallbackContext context = mockUserBelongingToOrganization(userIdentity); | |||
mockInstallations(); | |||
underTest.callback(context); | |||
verify(context).authenticate(userIdentity); | |||
verify(context).redirectToRequestedPage(); | |||
} | |||
@Test | |||
public void callback_whenOrganizationsAreNotDefinedAndUserDoesntBelongToInstallationOrganization_shouldThrow() throws IOException, ExecutionException, InterruptedException { | |||
UserIdentity userIdentity = mock(UserIdentity.class); | |||
CallbackContext context = mockUserNotBelongingToOrganization(userIdentity); | |||
mockInstallations(); | |||
assertThatThrownBy(() -> underTest.callback(context)) | |||
.isInstanceOf(UnauthorizedException.class) | |||
.hasMessage("'login' must be a member of at least one organization which has installed the SonarQube GitHub app"); | |||
} | |||
private CallbackContext mockUserBelongingToOrganization(UserIdentity userIdentity) throws IOException, InterruptedException, ExecutionException { | |||
setSettings(true); | |||
settings.setProperty("sonar.auth.github.organizations", ""); | |||
assertThat(underTest.isOrganizationMembershipRequired()).isFalse(); | |||
CallbackContext context = mock(); | |||
HttpRequest httpRequest = mock(); | |||
OAuth20Service scribeService = mock(); | |||
GsonUser user = new GsonUser("id", "login", "name", "email"); | |||
OAuth2AccessToken accessToken = mockAccessToken(scribeService, context, httpRequest, user); | |||
when(gitHubRestClient.isOrganizationMember(scribeService, accessToken, "organization1", "login")).thenReturn(false); | |||
when(gitHubRestClient.isOrganizationMember(scribeService, accessToken, "organization2", "login")).thenReturn(true); | |||
when(userIdentityFactory.create(user, "email", null)).thenReturn(userIdentity); | |||
return context; | |||
} | |||
private CallbackContext mockUserNotBelongingToOrganization(UserIdentity userIdentity) throws IOException, InterruptedException, ExecutionException { | |||
setSettings(true); | |||
CallbackContext context = mock(); | |||
HttpRequest httpRequest = mock(); | |||
OAuth20Service scribeService = mock(); | |||
GsonUser user = new GsonUser("id", "login", "name", "email"); | |||
OAuth2AccessToken accessToken = mockAccessToken(scribeService, context, httpRequest, user); | |||
when(gitHubRestClient.isOrganizationMember(scribeService, accessToken, "organization1", "login")).thenReturn(false); | |||
when(gitHubRestClient.isOrganizationMember(scribeService, accessToken, "organization2", "login")).thenReturn(false); | |||
when(userIdentityFactory.create(user, "email", null)).thenReturn(userIdentity); | |||
return context; | |||
} | |||
private OAuth2AccessToken mockAccessToken(OAuth20Service scribeService, CallbackContext context, HttpRequest httpRequest, GsonUser user) | |||
throws IOException, InterruptedException, ExecutionException { | |||
String callbackUrl = "http://localhost/callback"; | |||
when(scribeServiceBuilder.buildScribeService("id", "secret", callbackUrl, scribeApi)).thenReturn(scribeService); | |||
when(context.getHttpRequest()).thenReturn(httpRequest); | |||
when(context.getCallbackUrl()).thenReturn(callbackUrl); | |||
when(httpRequest.getParameter("code")).thenReturn("code"); | |||
OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class); | |||
when(scribeService.getAccessToken("code")).thenReturn(accessToken); | |||
when(gitHubRestClient.getUser(scribeService, accessToken)).thenReturn(user); | |||
return accessToken; | |||
} | |||
private void mockInstallations() { | |||
when(githubAppClient.getWhitelistedGithubAppInstallations(any())).thenReturn(List.of( | |||
new GithubAppInstallation("1", "organization1", new Permissions(), false), | |||
new GithubAppInstallation("2", "organization2", new Permissions(), false))); | |||
} | |||
private void setSettings(boolean enabled) { | |||
if (enabled) { | |||
settings.setProperty("sonar.auth.github.clientId.secured", "id"); | |||
settings.setProperty("sonar.auth.github.clientSecret.secured", "secret"); | |||
settings.setProperty("sonar.auth.github.enabled", true); | |||
settings.setProperty(CLIENT_ID, "id"); | |||
settings.setProperty(CLIENT_SECRET, "secret"); | |||
settings.setProperty(ENABLED, true); | |||
settings.setProperty(APP_ID, "1"); | |||
settings.setProperty(PRIVATE_KEY, "private"); | |||
} else { | |||
settings.setProperty("sonar.auth.github.enabled", false); | |||
settings.setProperty(ENABLED, false); | |||
} | |||
} | |||
} |
@@ -30,7 +30,7 @@ public class GitHubModuleTest { | |||
public void verify_count_of_added_components() { | |||
ListContainer container = new ListContainer(); | |||
new GitHubModule().configure(container); | |||
assertThat(container.getAddedObjects()).hasSize(15); | |||
assertThat(container.getAddedObjects()).hasSize(16); | |||
} | |||
} |
@@ -17,7 +17,7 @@ | |||
* 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.alm.client.github.config; | |||
package org.sonar.auth.github; | |||
import com.tngtech.java.junit.dataprovider.DataProvider; | |||
import com.tngtech.java.junit.dataprovider.DataProviderRunner; |
@@ -22,6 +22,7 @@ package org.sonar.auth.github; | |||
import java.io.IOException; | |||
import java.net.URLEncoder; | |||
import java.nio.charset.StandardCharsets; | |||
import java.util.List; | |||
import java.util.TreeSet; | |||
import java.util.concurrent.atomic.AtomicBoolean; | |||
import javax.servlet.http.HttpServletRequest; | |||
@@ -40,6 +41,8 @@ import org.sonar.api.server.authentication.UserIdentity; | |||
import org.sonar.api.server.http.HttpRequest; | |||
import org.sonar.api.server.http.HttpResponse; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.auth.github.scribe.ScribeServiceBuilder; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.server.http.JavaxHttpRequest; | |||
import org.sonar.server.property.InternalProperties; | |||
@@ -47,7 +50,9 @@ import org.sonar.server.property.InternalPropertiesImpl; | |||
import static java.lang.String.format; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | |||
import static org.assertj.core.api.Assertions.fail; | |||
import static org.mockito.ArgumentMatchers.any; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.when; | |||
@@ -69,9 +74,13 @@ public class IntegrationTest { | |||
private ScribeGitHubApi scribeApi = new ScribeGitHubApi(gitHubSettings); | |||
private GitHubRestClient gitHubRestClient = new GitHubRestClient(gitHubSettings); | |||
private GithubApplicationClient githubAppClient = mock(); | |||
private ScribeServiceBuilder scribeServiceBuilder = new ScribeServiceBuilder(); | |||
private String gitHubUrl; | |||
private GitHubIdentityProvider underTest = new GitHubIdentityProvider(gitHubSettings, userIdentityFactory, scribeApi, gitHubRestClient); | |||
private GitHubIdentityProvider underTest = new GitHubIdentityProvider(gitHubSettings, userIdentityFactory, scribeApi, gitHubRestClient, githubAppClient, scribeServiceBuilder); | |||
@Before | |||
public void enable() { | |||
@@ -81,6 +90,8 @@ public class IntegrationTest { | |||
settings.setProperty("sonar.auth.github.enabled", true); | |||
settings.setProperty("sonar.auth.github.apiUrl", gitHubUrl); | |||
settings.setProperty("sonar.auth.github.webUrl", gitHubUrl); | |||
settings.setProperty("sonar.auth.github.appId", "1"); | |||
settings.setProperty("sonar.auth.github.privateKey.secured", "private_key"); | |||
} | |||
/** | |||
@@ -115,9 +126,15 @@ public class IntegrationTest { | |||
github.enqueue(newSuccessfulAccessTokenResponse()); | |||
// response of api.github.com/user | |||
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":\"octocat@github.com\"}")); | |||
// response of api.github.com/orgs/first_org/members/user | |||
github.enqueue(new MockResponse().setResponseCode(404)); | |||
// response of api.github.com/orgs/second_org/members/user | |||
github.enqueue(new MockResponse().setResponseCode(204)); | |||
HttpServletRequest request = newRequest("the-verifier-code"); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext(request); | |||
mockInstallations(); | |||
underTest.callback(callbackContext); | |||
assertThat(callbackContext.csrfStateVerified.get()).isTrue(); | |||
@@ -146,6 +163,8 @@ public class IntegrationTest { | |||
github.enqueue(newSuccessfulAccessTokenResponse()); | |||
// response of api.github.com/user | |||
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":null}")); | |||
// response of api.github.com/orgs/first_org/members/user | |||
github.enqueue(new MockResponse().setResponseCode(204)); | |||
// response of api.github.com/user/emails | |||
github.enqueue(new MockResponse().setBody( | |||
"[\n" + | |||
@@ -163,6 +182,8 @@ public class IntegrationTest { | |||
HttpServletRequest request = newRequest("the-verifier-code"); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext(request); | |||
mockInstallations(); | |||
underTest.callback(callbackContext); | |||
assertThat(callbackContext.csrfStateVerified.get()).isTrue(); | |||
@@ -178,11 +199,16 @@ public class IntegrationTest { | |||
github.enqueue(newSuccessfulAccessTokenResponse()); | |||
// response of api.github.com/user | |||
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":null}")); | |||
// response of api.github.com/orgs/first_org/members/user | |||
github.enqueue(new MockResponse().setResponseCode(204)); | |||
// response of api.github.com/user/emails | |||
github.enqueue(new MockResponse().setBody("[]")); | |||
HttpServletRequest request = newRequest("the-verifier-code"); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext(request); | |||
mockInstallations(); | |||
underTest.callback(callbackContext); | |||
assertThat(callbackContext.csrfStateVerified.get()).isTrue(); | |||
@@ -215,6 +241,8 @@ public class IntegrationTest { | |||
github.enqueue(newSuccessfulAccessTokenResponse()); | |||
// response of api.github.com/user | |||
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":\"octocat@github.com\"}")); | |||
// response of api.github.com/orgs/first_org/members/user | |||
github.enqueue(new MockResponse().setResponseCode(204)); | |||
// response of api.github.com/user/teams | |||
github.enqueue(new MockResponse().setBody("[\n" + | |||
" {\n" + | |||
@@ -227,6 +255,8 @@ public class IntegrationTest { | |||
HttpServletRequest request = newRequest("the-verifier-code"); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext(request); | |||
mockInstallations(); | |||
underTest.callback(callbackContext); | |||
assertThat(callbackContext.userIdentity.getGroups()).containsOnly("SonarSource/developers"); | |||
@@ -239,6 +269,8 @@ public class IntegrationTest { | |||
github.enqueue(newSuccessfulAccessTokenResponse()); | |||
// response of api.github.com/user | |||
github.enqueue(new MockResponse().setBody("{\"id\":\"ABCD\", \"login\":\"octocat\", \"name\":\"monalisa octocat\",\"email\":\"octocat@github.com\"}")); | |||
// response of api.github.com/orgs/first_org/members/user | |||
github.enqueue(new MockResponse().setResponseCode(204)); | |||
// responses of api.github.com/user/teams | |||
github.enqueue(new MockResponse() | |||
.setHeader("Link", "<" + gitHubUrl + "/user/teams?per_page=100&page=2>; rel=\"next\", <" + gitHubUrl + "/user/teams?per_page=100&page=2>; rel=\"last\"") | |||
@@ -263,6 +295,8 @@ public class IntegrationTest { | |||
HttpServletRequest request = newRequest("the-verifier-code"); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext(request); | |||
mockInstallations(); | |||
underTest.callback(callbackContext); | |||
assertThat(new TreeSet<>(callbackContext.userIdentity.getGroups())).containsOnly("SonarQubeCommunity/sonarsource-developers", "SonarSource/developers"); | |||
@@ -316,12 +350,9 @@ public class IntegrationTest { | |||
HttpServletRequest request = newRequest("the-verifier-code"); | |||
DumbCallbackContext callbackContext = new DumbCallbackContext(request); | |||
try { | |||
underTest.callback(callbackContext); | |||
fail("exception expected"); | |||
} catch (UnauthorizedException e) { | |||
assertThat(e.getMessage()).contains("'octocat' must be a member of at least one organization:", "'first_org'", "'second_org'"); | |||
} | |||
assertThatThrownBy(() -> underTest.callback(callbackContext)) | |||
.isInstanceOf(UnauthorizedException.class) | |||
.hasMessage("'octocat' must be a member of at least one organization: 'first_org', 'second_org'"); | |||
} | |||
@Test | |||
@@ -378,6 +409,12 @@ public class IntegrationTest { | |||
return request; | |||
} | |||
private void mockInstallations() { | |||
when(githubAppClient.getWhitelistedGithubAppInstallations(any())).thenReturn(List.of( | |||
new GithubAppInstallation("1", "first_org", new GithubBinding.Permissions(), false), | |||
new GithubAppInstallation("2", "second_org", new GithubBinding.Permissions(), false))); | |||
} | |||
private static class DumbCallbackContext implements OAuth2IdentityProvider.CallbackContext { | |||
final HttpServletRequest request; | |||
final AtomicBoolean csrfStateVerified = new AtomicBoolean(false); |
@@ -25,10 +25,10 @@ import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.mockito.ArgumentCaptor; | |||
import org.sonar.alm.client.github.AppInstallationToken; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.auth.github.AppInstallationToken; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubApplicationClientImpl; | |||
import org.sonar.alm.client.github.api.GsonRepositoryCollaborator; | |||
import org.sonar.auth.github.GsonRepositoryCollaborator; | |||
import org.sonar.api.resources.Qualifiers; | |||
import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.utils.System2; |
@@ -24,9 +24,9 @@ import org.junit.BeforeClass; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.mockito.Mockito; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubApplicationClientImpl; | |||
import org.sonar.alm.client.github.security.UserAccessToken; | |||
import org.sonar.auth.github.security.UserAccessToken; | |||
import org.sonar.api.config.internal.Encryption; | |||
import org.sonar.api.config.internal.Settings; | |||
import org.sonar.api.server.ws.WebService; |
@@ -22,7 +22,7 @@ package org.sonar.server.almintegration.ws.github; | |||
import java.util.stream.Stream; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubApplicationClientImpl; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.db.DbTester; |
@@ -27,7 +27,7 @@ import org.apache.commons.io.IOUtils; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubGlobalSettingsValidator; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.auth.github.GitHubSettings; |
@@ -21,8 +21,8 @@ package org.sonar.server.almintegration.ws.github; | |||
import java.util.Optional; | |||
import javax.inject.Inject; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.alm.client.github.security.UserAccessToken; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonar.auth.github.security.UserAccessToken; | |||
import org.sonar.api.server.ws.Change; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; |
@@ -21,11 +21,11 @@ package org.sonar.server.almintegration.ws.github; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubApplicationClient.Organization; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.auth.github.client.GithubApplicationClient.Organization; | |||
import org.sonar.alm.client.github.GithubApplicationClientImpl; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.alm.client.github.security.UserAccessToken; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonar.auth.github.security.UserAccessToken; | |||
import org.sonar.api.config.internal.Encryption; | |||
import org.sonar.api.config.internal.Settings; | |||
import org.sonar.api.server.ws.Request; |
@@ -27,11 +27,11 @@ import java.util.Set; | |||
import java.util.function.BinaryOperator; | |||
import java.util.function.Function; | |||
import java.util.stream.Collectors; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubApplicationClient.Repository; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.auth.github.client.GithubApplicationClient.Repository; | |||
import org.sonar.alm.client.github.GithubApplicationClientImpl; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.alm.client.github.security.UserAccessToken; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonar.auth.github.security.UserAccessToken; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; |
@@ -20,8 +20,8 @@ | |||
package org.sonar.server.almsettings.ws; | |||
import javax.annotation.Nullable; | |||
import org.sonar.alm.client.github.AppInstallationToken; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.auth.github.AppInstallationToken; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonar.db.alm.setting.AlmSettingDto; | |||
import org.sonar.server.user.UserSession; | |||
@@ -23,12 +23,12 @@ import java.util.Optional; | |||
import java.util.Set; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import org.sonar.alm.client.github.AppInstallationToken; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.auth.github.AppInstallationToken; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubPermissionConverter; | |||
import org.sonar.alm.client.github.api.GsonRepositoryCollaborator; | |||
import org.sonar.alm.client.github.api.GsonRepositoryTeam; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.auth.github.GsonRepositoryCollaborator; | |||
import org.sonar.auth.github.GsonRepositoryTeam; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonar.api.web.UserRole; | |||
import org.sonar.auth.github.GsonRepositoryPermissions; | |||
import org.sonar.db.DbClient; |
@@ -23,12 +23,12 @@ import java.util.Map; | |||
import java.util.Optional; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.alm.client.github.AppInstallationToken; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.auth.github.AppInstallationToken; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubGlobalSettingsValidator; | |||
import org.sonar.alm.client.github.GithubPermissionConverter; | |||
import org.sonar.alm.client.github.config.GithubAppConfiguration; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.auth.github.GithubAppConfiguration; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.auth.github.GitHubSettings; | |||
import org.sonar.db.DbClient; |
@@ -28,8 +28,8 @@ import org.mockito.Answers; | |||
import org.mockito.InjectMocks; | |||
import org.mockito.Mock; | |||
import org.mockito.junit.MockitoJUnitRunner; | |||
import org.sonar.alm.client.github.AppInstallationToken; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.auth.github.AppInstallationToken; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.alm.client.github.GithubGlobalSettingsValidator; | |||
import org.sonar.alm.client.github.GithubPermissionConverter; | |||
import org.sonar.auth.github.GitHubSettings; |
@@ -31,11 +31,11 @@ import org.mockito.ArgumentCaptor; | |||
import org.mockito.Captor; | |||
import org.mockito.Mock; | |||
import org.mockito.junit.MockitoJUnitRunner; | |||
import org.sonar.alm.client.github.AppInstallationToken; | |||
import org.sonar.alm.client.github.GithubApplicationClient; | |||
import org.sonar.alm.client.github.api.GsonRepositoryCollaborator; | |||
import org.sonar.alm.client.github.api.GsonRepositoryTeam; | |||
import org.sonar.alm.client.github.security.AccessToken; | |||
import org.sonar.auth.github.AppInstallationToken; | |||
import org.sonar.auth.github.client.GithubApplicationClient; | |||
import org.sonar.auth.github.GsonRepositoryCollaborator; | |||
import org.sonar.auth.github.GsonRepositoryTeam; | |||
import org.sonar.auth.github.security.AccessToken; | |||
import org.sonar.api.resources.Qualifiers; | |||
import org.sonar.api.web.UserRole; | |||
import org.sonar.alm.client.github.GithubPermissionConverter; |