Merge branch 'stable-3.1' Change-Id: I512cc1997056289080413a42494df5a5fa820855
diff --git a/README.md b/README.md index dae7dcb..de036fb 100644 --- a/README.md +++ b/README.md
@@ -18,6 +18,7 @@ * [Keycloak](http://www.keycloak.org/) * [LemonLDAP::NG](https://lemonldap-ng.org) * [Office365](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols) +* [Phabricator](https://secure.phabricator.com/book/phabcontrib/article/using_oauthserver/) See the [Wiki](https://github.com/davido/gerrit-oauth-provider/wiki) what it can do for you.
diff --git a/WORKSPACE b/WORKSPACE index aa896ac..9cb922b 100644 --- a/WORKSPACE +++ b/WORKSPACE
@@ -3,7 +3,7 @@ load("//:bazlets.bzl", "load_bazlets") load_bazlets( - commit = "20079f696c22c733053077b13c0dc6d9902e6b8e", + commit = "8dc0767541f16b35d2136eccebffd9ebe2b81133", #local_path = "/home/<user>/projects/bazlets", )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java index c28f932..80bc605 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
@@ -112,5 +112,12 @@ .annotatedWith(Exports.named(AirVantageOAuthService.CONFIG_SUFFIX)) .to(AirVantageOAuthService.class); } + + cfg = cfgFactory.getFromGerritConfig(pluginName + PhabricatorOAuthService.CONFIG_SUFFIX); + if (cfg.getString(InitOAuth.CLIENT_ID) != null) { + bind(OAuthServiceProvider.class) + .annotatedWith(Exports.named(PhabricatorOAuthService.CONFIG_SUFFIX)) + .to(PhabricatorOAuthService.class); + } } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java index 8c874e6..854ba5b 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java +++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
@@ -49,6 +49,7 @@ private final Section keycloakOAuthProviderSection; private final Section office365OAuthProviderSection; private final Section airVantageOAuthProviderSection; + private final Section phabricatorOAuthProviderSection; @Inject InitOAuth(ConsoleUI ui, Section.Factory sections, @PluginName String pluginName) { @@ -75,6 +76,8 @@ sections.get(PLUGIN_SECTION, pluginName + Office365OAuthService.CONFIG_SUFFIX); this.airVantageOAuthProviderSection = sections.get(PLUGIN_SECTION, pluginName + AirVantageOAuthService.CONFIG_SUFFIX); + this.phabricatorOAuthProviderSection = + sections.get(PLUGIN_SECTION, pluginName + PhabricatorOAuthService.CONFIG_SUFFIX); } @Override @@ -170,6 +173,14 @@ if (configureAirVantageOAuthProvider) { configureOAuth(airVantageOAuthProviderSection); } + + boolean configurePhabricatorOAuthProvider = + ui.yesno( + isConfigured(phabricatorOAuthProviderSection), + "Use Phabricator OAuth provider for Gerrit login ?"); + if (configurePhabricatorOAuthProvider && configureOAuth(phabricatorOAuthProviderSection)) { + checkRootUrl(phabricatorOAuthProviderSection.string("Phabricator Root URL", ROOT_URL, null)); + } } /**
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/PhabricatorApi.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/PhabricatorApi.java new file mode 100644 index 0000000..a2a52ff --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/PhabricatorApi.java
@@ -0,0 +1,44 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.oauth; + +import com.github.scribejava.core.builder.api.DefaultApi20; +import com.github.scribejava.core.oauth2.bearersignature.BearerSignature; +import com.github.scribejava.core.oauth2.bearersignature.BearerSignatureURIQueryParameter; + +public class PhabricatorApi extends DefaultApi20 { + private static final String AUTHORIZE_URL = "%s/oauthserver/auth/"; + + private final String rootUrl; + + public PhabricatorApi(String rootUrl) { + this.rootUrl = rootUrl; + } + + @Override + public String getAuthorizationBaseUrl() { + return String.format(AUTHORIZE_URL, rootUrl); + } + + @Override + public String getAccessTokenEndpoint() { + return String.format("%s/oauthserver/token/", rootUrl); + } + + @Override + public BearerSignature getBearerSignature() { + return BearerSignatureURIQueryParameter.instance(); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/PhabricatorOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/PhabricatorOAuthService.java new file mode 100644 index 0000000..18086ec --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/PhabricatorOAuthService.java
@@ -0,0 +1,156 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.oauth; + +import static com.google.gerrit.json.OutputFormat.JSON; + +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.Response; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.OAuth20Service; +import com.google.common.base.CharMatcher; +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; +import com.google.gerrit.extensions.auth.oauth.OAuthToken; +import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; +import com.google.gerrit.extensions.auth.oauth.OAuthVerifier; +import com.google.gerrit.server.config.CanonicalWebUrl; +import com.google.gerrit.server.config.PluginConfig; +import com.google.gerrit.server.config.PluginConfigFactory; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.ProvisionException; +import com.google.inject.Singleton; +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.ExecutionException; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +class PhabricatorOAuthService implements OAuthServiceProvider { + private static final Logger log = LoggerFactory.getLogger(PhabricatorOAuthService.class); + static final String CONFIG_SUFFIX = "-phabricator-oauth"; + private static final String PHABRICATOR_PROVIDER_PREFIX = "phabricator-oauth:"; + private static final String PROTECTED_RESOURCE_URL = "%s/api/user.whoami"; + private final String rootUrl; + private final OAuth20Service service; + + @Inject + PhabricatorOAuthService( + PluginConfigFactory cfgFactory, + @PluginName String pluginName, + @CanonicalWebUrl Provider<String> urlProvider) { + PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX); + String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/"; + rootUrl = cfg.getString(InitOAuth.ROOT_URL); + if (!URI.create(rootUrl).isAbsolute()) { + throw new ProvisionException("Root URL must be absolute URL"); + } + this.service = + new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID)) + .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET)) + .callback(canonicalWebUrl + "oauth") + .build(new PhabricatorApi(rootUrl)); + if (log.isDebugEnabled()) { + log.debug("OAuth2: canonicalWebUrl={}", canonicalWebUrl); + } + } + + @Override + public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException { + OAuthRequest request = + new OAuthRequest(Verb.GET, String.format(PROTECTED_RESOURCE_URL, rootUrl)); + OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw()); + service.signRequest(t, request); + + JsonElement userJson = null; + try (Response response = service.execute(request)) { + if (response.getCode() != HttpServletResponse.SC_OK) { + throw new IOException( + String.format( + "Status %s (%s) for request %s", + response.getCode(), response.getBody(), request.getUrl())); + } + userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class); + if (log.isDebugEnabled()) { + log.debug("User info response: {}", response.getBody()); + } + if (userJson.isJsonObject()) { + JsonObject jsonObject = userJson.getAsJsonObject(); + JsonElement jsonResult = jsonObject.get("result"); + if (jsonResult == null) { + throw new IOException("Response doesn't contain result field"); + } + JsonObject resultObject = jsonResult.getAsJsonObject(); + JsonElement id = resultObject.get("phid"); + if (id == null || id.isJsonNull()) { + throw new IOException("Response doesn't contain id field"); + } + JsonElement email = resultObject.get("primaryEmail"); + JsonElement name = resultObject.get("realName"); + JsonElement username = resultObject.get("userName"); + String login = null; + + if (!username.isJsonNull()) { + login = username.getAsString(); + } + return new OAuthUserInfo( + PHABRICATOR_PROVIDER_PREFIX + id.getAsString() /*externalId*/, + login /*username*/, + email == null || email.isJsonNull() ? null : email.getAsString() /*email*/, + name == null || name.isJsonNull() ? null : name.getAsString() /*displayName*/, + null); + } + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException("Cannot retrieve user info resource", e); + } + + throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson)); + } + + @Override + public OAuthToken getAccessToken(OAuthVerifier rv) { + try { + OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue()); + return new OAuthToken( + accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse()); + } catch (InterruptedException | ExecutionException | IOException e) { + String msg = "Cannot retrieve access token"; + log.error(msg, e); + throw new RuntimeException(msg, e); + } + } + + @Override + public String getAuthorizationUrl() { + return service.getAuthorizationUrl(); + } + + @Override + public String getVersion() { + return service.getVersion(); + } + + @Override + public String getName() { + return "Phabricator OAuth2"; + } +}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index a97fd16..19eb426 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -43,6 +43,11 @@ [plugin "@PLUGIN@-airvantage-oauth"] client-id = "<client-id>" client-secret = "<client-secret>" + + [plugin "@PLUGIN@-phabricator-oauth"] + client-id = "<client-id>" + client-secret = "<client-secret>" + root-url = "<phabricator url>" ``` When one from the sections above is omitted, OAuth SSO is used. @@ -211,3 +216,9 @@ The client-id and client-secret for AirVantage OAuth can be obtained by registering a Client application. See [Getting Started](https://source.sierrawireless.com/airvantage/av/howto/cloud/gettingstarted_api). + +### Phabricator + +The client-id and client-secret for Phabricator can be obtained by registering a +Client application. +See [Using the Phabricator OAuth Server](https://secure.phabricator.com/book/phabcontrib/article/using_oauthserver/).
diff --git a/src/test/java/com/googlesource/gerrit/plugins/oauth/PhabricatorApiTest.java b/src/test/java/com/googlesource/gerrit/plugins/oauth/PhabricatorApiTest.java new file mode 100644 index 0000000..9a9646c --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/oauth/PhabricatorApiTest.java
@@ -0,0 +1,35 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.oauth; + +import static com.google.common.truth.Truth.assertThat; + +import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor; +import org.junit.Before; +import org.junit.Test; + +public class PhabricatorApiTest { + private PhabricatorApi api; + + @Before + public void setUp() { + api = new PhabricatorApi(""); + } + + @Test + public void testAccessTokenExtractor() { + assertThat(api.getAccessTokenExtractor()).isInstanceOf(OAuth2AccessTokenJsonExtractor.class); + } +}