Merge branch 'stable-3.0' into stable-3.1

* stable-3.0:
  Upgrade bazlets to latest stable-2.16 to build with 2.16.20 API

Change-Id: Ibe62ddae5998e72f2d592832cbf6ebea7e5dbb8c
diff --git a/README.md b/README.md
index 7a156b2..dae7dcb 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@
 * [GitLab](https://about.gitlab.com/)
 * [Google](https://developers.google.com/identity/protocols/OAuth2)
 * [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)
 
 See the [Wiki](https://github.com/davido/gerrit-oauth-provider/wiki) what it can do for you.
diff --git a/WORKSPACE b/WORKSPACE
index 4e5f19d..81845cd 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,7 +3,7 @@
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "15eae2ee5cd524a204bd62c3d59bfd0ce86916ec",
+    commit = "51c1fd2758fcc1b900a80d4ebb037bc373f85d39",
     #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 4035a15..c28f932 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
@@ -78,6 +78,13 @@
           .to(GitLabOAuthService.class);
     }
 
+    cfg = cfgFactory.getFromGerritConfig(pluginName + LemonLDAPOAuthService.CONFIG_SUFFIX);
+    if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
+      bind(OAuthServiceProvider.class)
+          .annotatedWith(Exports.named(LemonLDAPOAuthService.CONFIG_SUFFIX))
+          .to(LemonLDAPOAuthService.class);
+    }
+
     cfg = cfgFactory.getFromGerritConfig(pluginName + DexOAuthService.CONFIG_SUFFIX);
     if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
       bind(OAuthServiceProvider.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 540b65d..8c874e6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
@@ -44,6 +44,7 @@
   private final Section casOAuthProviderSection;
   private final Section facebookOAuthProviderSection;
   private final Section gitlabOAuthProviderSection;
+  private final Section lemonldapOAuthProviderSection;
   private final Section dexOAuthProviderSection;
   private final Section keycloakOAuthProviderSection;
   private final Section office365OAuthProviderSection;
@@ -64,6 +65,8 @@
         sections.get(PLUGIN_SECTION, pluginName + FacebookOAuthService.CONFIG_SUFFIX);
     this.gitlabOAuthProviderSection =
         sections.get(PLUGIN_SECTION, pluginName + GitLabOAuthService.CONFIG_SUFFIX);
+    this.lemonldapOAuthProviderSection =
+        sections.get(PLUGIN_SECTION, pluginName + LemonLDAPOAuthService.CONFIG_SUFFIX);
     this.dexOAuthProviderSection =
         sections.get(PLUGIN_SECTION, pluginName + DexOAuthService.CONFIG_SUFFIX);
     this.keycloakOAuthProviderSection =
@@ -107,11 +110,7 @@
         ui.yesno(
             isConfigured(casOAuthProviderSection), "Use CAS OAuth provider for Gerrit login ?");
     if (configureCasOAuthProvider && configureOAuth(casOAuthProviderSection)) {
-      String rootUrl = casOAuthProviderSection.string("CAS Root URL", ROOT_URL, null);
-      requireNonNull(rootUrl);
-      if (!URI.create(rootUrl).isAbsolute()) {
-        throw new ProvisionException("Root URL must be absolute URL");
-      }
+      checkRootUrl(casOAuthProviderSection.string("CAS Root URL", ROOT_URL, null));
       casOAuthProviderSection.string(FIX_LEGACY_USER_ID_QUESTION, FIX_LEGACY_USER_ID, "false");
     }
 
@@ -128,22 +127,23 @@
             isConfigured(gitlabOAuthProviderSection),
             "Use GitLab OAuth provider for Gerrit login ?");
     if (configureGitLabOAuthProvider && configureOAuth(gitlabOAuthProviderSection)) {
-      String rootUrl = gitlabOAuthProviderSection.string("GitLab Root URL", ROOT_URL, null);
-      requireNonNull(rootUrl);
-      if (!URI.create(rootUrl).isAbsolute()) {
-        throw new ProvisionException("Root URL must be absolute URL");
-      }
+      checkRootUrl(gitlabOAuthProviderSection.string("GitLab Root URL", ROOT_URL, null));
+    }
+
+    boolean configureLemonLDAPOAuthProvider =
+        ui.yesno(
+            isConfigured(lemonldapOAuthProviderSection),
+            "Use LemonLDAP OAuth provider for Gerrit login ?");
+    if (configureLemonLDAPOAuthProvider) {
+      checkRootUrl(lemonldapOAuthProviderSection.string("LemonLDAP Root URL", ROOT_URL, null));
+      configureOAuth(lemonldapOAuthProviderSection);
     }
 
     boolean configureDexOAuthProvider =
         ui.yesno(
             isConfigured(dexOAuthProviderSection), "Use Dex OAuth provider for Gerrit login ?");
     if (configureDexOAuthProvider && configureOAuth(dexOAuthProviderSection)) {
-      String rootUrl = dexOAuthProviderSection.string("Dex Root URL", ROOT_URL, null);
-      requireNonNull(rootUrl);
-      if (!URI.create(rootUrl).isAbsolute()) {
-        throw new ProvisionException("Root URL must be absolute URL");
-      }
+      checkRootUrl(dexOAuthProviderSection.string("Dex Root URL", ROOT_URL, null));
     }
 
     boolean configureKeycloakOAuthProvider =
@@ -151,11 +151,7 @@
             isConfigured(keycloakOAuthProviderSection),
             "Use Keycloak OAuth provider for Gerrit login ?");
     if (configureKeycloakOAuthProvider && configureOAuth(keycloakOAuthProviderSection)) {
-      String rootUrl = keycloakOAuthProviderSection.string("Keycloak Root URL", ROOT_URL, null);
-      requireNonNull(rootUrl);
-      if (!URI.create(rootUrl).isAbsolute()) {
-        throw new ProvisionException("Root URL must be absolute URL");
-      }
+      checkRootUrl(keycloakOAuthProviderSection.string("Keycloak Root URL", ROOT_URL, null));
       keycloakOAuthProviderSection.string("Keycloak Realm", REALM, null);
     }
 
@@ -200,6 +196,19 @@
     return false;
   }
 
+  /**
+   * Check root URL parameter. It must be not null and it must be an absolute URI.
+   *
+   * @param rootUrl root URL
+   * @throws ProvisionException if rootUrl wasn't provided or is not absolute URI.
+   */
+  private static void checkRootUrl(String rootUrl) {
+    requireNonNull(rootUrl);
+    if (!URI.create(rootUrl).isAbsolute()) {
+      throw new ProvisionException("Root URL must be absolute URL");
+    }
+  }
+
   @Override
   public void postRun() throws Exception {}
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/LemonLDAPApi.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/LemonLDAPApi.java
new file mode 100644
index 0000000..7b66c25
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/LemonLDAPApi.java
@@ -0,0 +1,55 @@
+// 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;
+import com.github.scribejava.core.oauth2.clientauthentication.ClientAuthentication;
+import com.github.scribejava.core.oauth2.clientauthentication.RequestBodyAuthenticationScheme;
+
+public class LemonLDAPApi extends DefaultApi20 {
+  private static final String AUTHORIZE_URL = "%s/oauth2/authorize";
+
+  private final String rootUrl;
+
+  public LemonLDAPApi(String rootUrl) {
+    this.rootUrl = rootUrl;
+  }
+
+  @Override
+  public String getAuthorizationBaseUrl() {
+    return String.format(AUTHORIZE_URL, rootUrl);
+  }
+
+  @Override
+  public String getAccessTokenEndpoint() {
+    return String.format("%s/oauth2/token", rootUrl);
+  }
+
+  // TODO(davido): Remove this override, if HttpBasicAuthentication
+  // scheme is supported.
+  @Override
+  public ClientAuthentication getClientAuthentication() {
+    return RequestBodyAuthenticationScheme.instance();
+  }
+
+  // TODO(davido): Remove this override, if BearerSignatureAuthorization
+  // request header field is supported.
+  @Override
+  public BearerSignature getBearerSignature() {
+    return BearerSignatureURIQueryParameter.instance();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/LemonLDAPOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/LemonLDAPOAuthService.java
new file mode 100644
index 0000000..f1a0dbe
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/LemonLDAPOAuthService.java
@@ -0,0 +1,134 @@
+// 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 static org.slf4j.LoggerFactory.getLogger;
+
+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.Singleton;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+
+@Singleton
+public class LemonLDAPOAuthService implements OAuthServiceProvider {
+  private static final Logger log = getLogger(LemonLDAPOAuthService.class);
+  static final String CONFIG_SUFFIX = "-lemonldap-oauth";
+  private static final String PROTECTED_RESOURCE_URL = "%s/oauth2/userinfo";
+  private static final String LEMONLDAP_PROVIDER_PREFIX = "llng-oauth:";
+  private final OAuth20Service service;
+  private final String rootUrl;
+
+  @Inject
+  LemonLDAPOAuthService(
+      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);
+    service =
+        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
+            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+            .defaultScope("openid+email+profile")
+            .callback(canonicalWebUrl + "oauth")
+            .build(new LemonLDAPApi(rootUrl));
+  }
+
+  @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);
+
+    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()));
+      }
+      JsonElement userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class);
+      if (log.isDebugEnabled()) {
+        log.debug("User info response: {}", response.getBody());
+      }
+      JsonObject jsonObject = userJson.getAsJsonObject();
+      if (jsonObject == null || jsonObject.isJsonNull()) {
+        throw new IOException("Response doesn't contain 'user' field" + jsonObject);
+      }
+      JsonElement id = jsonObject.get("sub");
+      JsonElement username = jsonObject.get("username");
+      JsonElement email = jsonObject.get("email");
+      JsonElement name = jsonObject.get("name");
+      return new OAuthUserInfo(
+          LEMONLDAP_PROVIDER_PREFIX + id.getAsString(),
+          username == null || username.isJsonNull() ? null : username.getAsString(),
+          email == null || email.isJsonNull() ? null : email.getAsString(),
+          name == null || name.isJsonNull() ? null : name.getAsString(),
+          null);
+    } catch (ExecutionException | InterruptedException e) {
+      throw new RuntimeException("Cannot retrieve user info resource", e);
+    }
+  }
+
+  @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 "LemonLDAP::NG OAuth2 provider";
+  }
+}