Introduce Bearer Token Authentication The Bearer Token Authentication allows replication without the use of an account Extend functionality of FetchRestApiClient to provide Bearer Token Authentication Provide a new filter BearerAuthenticationFilter to authenticate Pull Replication API calls with Bearer Token Bug: Issue 15605 Change-Id: I218eebe9f1628d16e75b4dc60a0cabf51b18d9df
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/BearerTokenProvider.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/BearerTokenProvider.java new file mode 100644 index 0000000..be34319 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/BearerTokenProvider.java
@@ -0,0 +1,38 @@ +// Copyright (C) 2022 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.replication.pull; + +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.util.Optional; +import org.eclipse.jgit.lib.Config; + +@Singleton +public class BearerTokenProvider implements Provider<Optional<String>> { + + private final Optional<String> bearerToken; + + @Inject + public BearerTokenProvider(@GerritServerConfig Config gerritConfig) { + this.bearerToken = Optional.ofNullable(gerritConfig.getString("auth", null, "bearerToken")); + } + + @Override + public Optional<String> get() { + return bearerToken; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java index 3e51e03..93bbde0 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
@@ -73,6 +73,7 @@ @Override protected void configure() { + bind(BearerTokenProvider.class).in(Scopes.SINGLETON); bind(RevisionReader.class).in(Scopes.SINGLETON); bind(ApplyObject.class); install(new FactoryModuleBuilder().build(FetchJob.Factory.class));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java new file mode 100644 index 0000000..cbe1ab8 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
@@ -0,0 +1,133 @@ +// Copyright (C) 2022 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.replication.pull.api; + +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.httpd.AllRequestFilter; +import com.google.gerrit.httpd.WebSession; +import com.google.gerrit.server.AccessPath; +import com.google.gerrit.server.PluginUser; +import com.google.gerrit.server.util.ManualRequestContext; +import com.google.gerrit.server.util.ThreadLocalRequestContext; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.name.Named; +import java.io.IOException; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Authenticates the current user by HTTP bearer token authentication. + * + * <p>* @see <a href="https://www.rfc-editor.org/rfc/rfc6750">RFC 6750</a> + */ +public class BearerAuthenticationFilter extends AllRequestFilter { + + private static final String BEARER_TOKEN = "BearerToken"; + private final DynamicItem<WebSession> session; + private final String pluginName; + private final Provider<PluginUser> pluginUserProvider; + private final Provider<ThreadLocalRequestContext> threadLocalRequestContext; + private final String bearerToken; + private final Pattern bearerTokenRegex = Pattern.compile("^Bearer\\s(.+)$"); + + @Inject + BearerAuthenticationFilter( + DynamicItem<WebSession> session, + @PluginName String pluginName, + Provider<PluginUser> pluginUserProvider, + Provider<ThreadLocalRequestContext> threadLocalRequestContext, + @Named(BEARER_TOKEN) String bearerToken) { + this.session = session; + this.pluginName = pluginName; + this.pluginUserProvider = pluginUserProvider; + this.threadLocalRequestContext = threadLocalRequestContext; + this.bearerToken = bearerToken; + } + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + + if (!(servletRequest instanceof HttpServletRequest) + || !(servletResponse instanceof HttpServletResponse)) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; + String requestURI = httpRequest.getRequestURI(); + + if (isBasicAuthenticationRequest(requestURI)) { + filterChain.doFilter(servletRequest, servletResponse); + } else if (isPullReplicationApiRequest(requestURI)) { + Optional<String> authorizationHeader = + Optional.ofNullable(httpRequest.getHeader("Authorization")); + + if (isBearerTokenAuthenticated(authorizationHeader, bearerToken)) + try (ManualRequestContext ctx = + new ManualRequestContext(pluginUserProvider.get(), threadLocalRequestContext.get())) { + WebSession ws = session.get(); + ws.setAccessPathOk(AccessPath.REST_API, true); + filterChain.doFilter(servletRequest, servletResponse); + } + else httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); + + } else { + filterChain.doFilter(servletRequest, servletResponse); + } + } + + private boolean isBearerTokenAuthenticated( + Optional<String> authorizationHeader, String bearerToken) { + return authorizationHeader + .flatMap(this::extractBearerToken) + .map(bt -> bt.equals(bearerToken)) + .orElse(false); + } + + private boolean isBasicAuthenticationRequest(String requestURI) { + return requestURI.startsWith("/a/"); + } + + private boolean isPullReplicationApiRequest(String requestURI) { + return (requestURI.contains(pluginName) + && (requestURI.endsWith(String.format("/%s~apply-object", pluginName)) + || requestURI.endsWith(String.format("/%s~apply-objects", pluginName)) + || requestURI.endsWith(String.format("/%s~fetch", pluginName)) + || requestURI.endsWith(String.format("/%s~delete-project", pluginName)) + || requestURI.contains(String.format("/%s/init-project/", pluginName)))) + || requestURI.matches(".*/projects/[^/]+/HEAD"); + } + + private Optional<String> extractBearerToken(String authorizationHeader) { + Matcher projectGroupMatcher = bearerTokenRegex.matcher(authorizationHeader); + + if (projectGroupMatcher.find()) { + return Optional.of(projectGroupMatcher.group(1)); + } + return Optional.empty(); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java index 83e8487..0f3e1e8 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpModule.java
@@ -19,14 +19,18 @@ import com.google.gerrit.server.config.GerritIsReplica; import com.google.inject.Inject; import com.google.inject.Scopes; +import com.google.inject.name.Names; import com.google.inject.servlet.ServletModule; +import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider; public class HttpModule extends ServletModule { private boolean isReplica; + private final BearerTokenProvider bearerTokenProvider; @Inject - public HttpModule(@GerritIsReplica Boolean isReplica) { + public HttpModule(@GerritIsReplica Boolean isReplica, BearerTokenProvider bearerTokenProvider) { this.isReplica = isReplica; + this.bearerTokenProvider = bearerTokenProvider; } @Override @@ -35,6 +39,16 @@ .to(PullReplicationApiMetricsFilter.class) .in(Scopes.SINGLETON); + bearerTokenProvider + .get() + .ifPresent( + bt -> { + bind(String.class).annotatedWith(Names.named("BearerToken")).toInstance(bt); + DynamicSet.bind(binder(), AllRequestFilter.class) + .to(BearerAuthenticationFilter.class) + .in(Scopes.SINGLETON); + }); + if (isReplica) { DynamicSet.bind(binder(), AllRequestFilter.class) .to(PullReplicationFilter.class)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java index c1174c9..2214fb3 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java
@@ -108,9 +108,4 @@ Project.NameKey projectNameKey = Project.NameKey.parse(projectName); return localFS.createProject(projectNameKey, RefNames.HEAD); } - - public static String getProjectInitializationUrl(String pluginName, String projectName) { - return String.format( - "a/plugins/%s/init-project/%s", pluginName, Url.encode(projectName) + ".git"); - } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java index 09139f0..0afbecf 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
@@ -15,7 +15,6 @@ package com.googlesource.gerrit.plugins.replication.pull.client; import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES; -import static com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction.getProjectInitializationUrl; import static java.util.Objects.requireNonNull; import com.google.common.base.Strings; @@ -33,6 +32,7 @@ import com.google.inject.assistedinject.Assisted; import com.googlesource.gerrit.plugins.replication.CredentialsFactory; import com.googlesource.gerrit.plugins.replication.ReplicationConfig; +import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider; import com.googlesource.gerrit.plugins.replication.pull.Source; import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics; import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData; @@ -43,6 +43,7 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; +import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.ParseException; import org.apache.http.auth.AuthenticationException; @@ -73,6 +74,8 @@ private final String instanceId; private final String pluginName; private final SyncRefsFilter syncRefsFilter; + private final BearerTokenProvider bearerTokenProvider; + private final String urlAuthenticationPrefix; @Inject FetchRestApiClient( @@ -82,6 +85,7 @@ SyncRefsFilter syncRefsFilter, @PluginName String pluginName, @Nullable @GerritInstanceId String instanceId, + BearerTokenProvider bearerTokenProvider, @Assisted Source source) { this.credentials = credentials; this.httpClientFactory = httpClientFactory; @@ -97,6 +101,9 @@ requireNonNull( Strings.emptyToNull(this.instanceId), "gerrit.instanceId or replication.instanceLabel must be set"); + + this.bearerTokenProvider = bearerTokenProvider; + this.urlAuthenticationPrefix = bearerTokenProvider.get().map(br -> "").orElse("a/"); } /* (non-Javadoc) @@ -105,11 +112,8 @@ @Override public HttpResult callFetch( Project.NameKey project, String refName, URIish targetUri, long startTimeNanos) - throws ClientProtocolException, IOException { - String url = - String.format( - "%s/a/projects/%s/pull-replication~fetch", - targetUri.toString(), Url.encode(project.get())); + throws IOException { + String url = formatUrl(targetUri.toString(), project, "fetch"); Boolean callAsync = !syncRefsFilter.match(refName); HttpPost post = new HttpPost(url); post.setEntity( @@ -122,7 +126,7 @@ post.addHeader( PullReplicationApiRequestMetrics.HTTP_HEADER_X_START_TIME_NANOS, Long.toString(startTimeNanos)); - return httpClientFactory.create(source).execute(withBasicAuthentication(targetUri, post), this); + return executeRequest(post, bearerTokenProvider.get(), targetUri); } /* (non-Javadoc) @@ -130,13 +134,11 @@ */ @Override public HttpResult initProject(Project.NameKey project, URIish uri) throws IOException { - String url = - String.format( - "%s/%s", uri.toString(), getProjectInitializationUrl(pluginName, project.get())); + String url = formatInitProjectUrl(uri.toString(), project); HttpPut put = new HttpPut(url); put.addHeader(new BasicHeader("Accept", MediaType.ANY_TEXT_TYPE.toString())); put.addHeader(new BasicHeader("Content-Type", MediaType.PLAIN_TEXT_UTF_8.toString())); - return httpClientFactory.create(source).execute(withBasicAuthentication(uri, put), this); + return executeRequest(put, bearerTokenProvider.get(), uri); } /* (non-Javadoc) @@ -144,10 +146,9 @@ */ @Override public HttpResult deleteProject(Project.NameKey project, URIish apiUri) throws IOException { - String url = - String.format("%s/%s", apiUri.toASCIIString(), getProjectDeletionUrl(project.get())); + String url = formatUrl(apiUri.toASCIIString(), project, "delete-project"); HttpDelete delete = new HttpDelete(url); - return httpClientFactory.create(source).execute(withBasicAuthentication(apiUri, delete), this); + return executeRequest(delete, bearerTokenProvider.get(), apiUri); } /* (non-Javadoc) @@ -157,13 +158,12 @@ public HttpResult updateHead(Project.NameKey project, String newHead, URIish apiUri) throws IOException { logger.atFine().log("Updating head of %s on %s", project.get(), newHead); - String url = - String.format("%s/%s", apiUri.toASCIIString(), getProjectUpdateHeadUrl(project.get())); + String url = formatUrl(apiUri.toASCIIString(), project, "HEAD"); HttpPut req = new HttpPut(url); req.setEntity( new StringEntity(String.format("{\"ref\": \"%s\"}", newHead), StandardCharsets.UTF_8)); req.addHeader(new BasicHeader("Content-Type", MediaType.JSON_UTF_8.toString())); - return httpClientFactory.create(source).execute(withBasicAuthentication(apiUri, req), this); + return executeRequest(req, bearerTokenProvider.get(), apiUri); } /* (non-Javadoc) @@ -186,12 +186,12 @@ } RevisionInput input = new RevisionInput(instanceId, refName, revisionData); - String url = formatUrl(project, targetUri, "apply-object"); + String url = formatUrl(targetUri.toString(), project, "apply-object"); HttpPost post = new HttpPost(url); post.setEntity(new StringEntity(GSON.toJson(input))); post.addHeader(new BasicHeader("Content-Type", MediaType.JSON_UTF_8.toString())); - return httpClientFactory.create(source).execute(withBasicAuthentication(targetUri, post), this); + return executeRequest(post, bearerTokenProvider.get(), targetUri); } @Override @@ -205,19 +205,23 @@ RevisionData[] inputData = new RevisionData[revisionData.size()]; RevisionsInput input = new RevisionsInput(instanceId, refName, revisionData.toArray(inputData)); - String url = formatUrl(project, targetUri, "apply-objects"); + String url = formatUrl(targetUri.toString(), project, "apply-objects"); HttpPost post = new HttpPost(url); post.setEntity(new StringEntity(GSON.toJson(input))); post.addHeader(new BasicHeader("Content-Type", MediaType.JSON_UTF_8.toString())); - return httpClientFactory.create(source).execute(withBasicAuthentication(targetUri, post), this); + return executeRequest(post, bearerTokenProvider.get(), targetUri); } - private String formatUrl(Project.NameKey project, URIish targetUri, String api) { - String url = - String.format( - "%s/a/projects/%s/%s~%s", - targetUri.toString(), Url.encode(project.get()), pluginName, api); - return url; + private String formatUrl(String targetUri, Project.NameKey project, String api) { + return String.format( + "%s/%sprojects/%s/%s~%s", + targetUri, urlAuthenticationPrefix, Url.encode(project.get()), pluginName, api); + } + + private String formatInitProjectUrl(String targetUri, Project.NameKey project) { + return String.format( + "%s/%splugins/%s/init-project/%s.git", + targetUri, urlAuthenticationPrefix, pluginName, Url.encode(project.get())); } private void requireNull(Object object, String string) { @@ -245,6 +249,18 @@ return new HttpResult(response.getStatusLine().getStatusCode(), responseBody); } + private HttpResult executeRequest( + HttpRequestBase httpRequest, Optional<String> bearerToken, URIish targetUri) + throws IOException { + + HttpRequestBase reqWithAuthentication = + bearerToken.isPresent() + ? withBearerTokenAuthentication(httpRequest, bearerToken.get()) + : withBasicAuthentication(targetUri, httpRequest); + + return httpClientFactory.create(source).execute(reqWithAuthentication, this); + } + private HttpRequestBase withBasicAuthentication(URIish targetUri, HttpRequestBase req) { org.eclipse.jgit.transport.CredentialsProvider cp = credentials.create(source.getRemoteConfigName()); @@ -262,11 +278,8 @@ return req; } - String getProjectDeletionUrl(String projectName) { - return String.format("a/projects/%s/%s~delete-project", Url.encode(projectName), pluginName); - } - - String getProjectUpdateHeadUrl(String projectName) { - return String.format("a/projects/%s/%s~HEAD", Url.encode(projectName), pluginName); + private HttpRequestBase withBearerTokenAuthentication(HttpRequestBase req, String bearerToken) { + req.addHeader(new BasicHeader(HttpHeaders.AUTHORIZATION, "Bearer " + bearerToken)); + return req; } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java index 55ad15c..20c1fea 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
@@ -42,6 +42,7 @@ import java.util.Base64; import java.util.List; import java.util.Optional; +import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.auth.AuthenticationException; import org.apache.http.auth.UsernamePasswordCredentials; @@ -82,7 +83,11 @@ SourceHttpClient.Factory httpClientFactory; String url; - protected abstract String getURL(String projectName); + protected abstract String getURLWithAuthenticationPrefix(String projectName); + + protected String getURLWithoutAuthenticationPrefix(String projectName) { + return getURLWithAuthenticationPrefix(projectName).replace("a/", ""); + } @Override public void setUpTestPlugin() throws Exception { @@ -108,7 +113,7 @@ revisionReader = plugin.getSysInjector().getInstance(RevisionReader.class); source = plugin.getSysInjector().getInstance(SourcesCollection.class).getAll().get(0); - url = getURL(project.get()); + url = getURLWithAuthenticationPrefix(project.get()); } protected HttpPost createRequest(String sendObjectPayload) { @@ -169,6 +174,12 @@ }; } + protected HttpRequestBase withBearerTokenAuthentication( + HttpRequestBase httpRequest, String bearerToken) { + httpRequest.addHeader(new BasicHeader(HttpHeaders.AUTHORIZATION, "Bearer " + bearerToken)); + return httpRequest; + } + protected HttpRequestBase withBasicAuthenticationAsAdmin(HttpRequestBase httpRequest) throws AuthenticationException { return withBasicAuthentication(httpRequest, admin);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java index a5fd63c..2ab5caf 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ApplyObjectActionIT.java
@@ -180,6 +180,56 @@ assertHttpResponseCode(400)); } + @Test + @GerritConfig(name = "container.replica", value = "true") + @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token") + public void shouldAcceptPayloadWhenNodeIsAReplicaWithBearerToken() throws Exception { + url = getURLWithoutAuthenticationPrefix(project.get()); + String payloadWithoutAsyncFieldTemplate = + "{\"label\":\"" + + TEST_REPLICATION_REMOTE + + "\",\"ref_name\":\"%s\",\"revision_data\":{\"commit_object\":{\"type\":1,\"content\":\"%s\"},\"tree_object\":{\"type\":2,\"content\":\"%s\"},\"blobs\":[]}}"; + + String refName = createRef(); + Optional<RevisionData> revisionDataOption = createRevisionData(refName); + assertThat(revisionDataOption.isPresent()).isTrue(); + + RevisionData revisionData = revisionDataOption.get(); + String sendObjectPayload = + createPayload(payloadWithoutAsyncFieldTemplate, refName, revisionData); + + httpClientFactory + .create(source) + .execute( + withBearerTokenAuthentication(createRequest(sendObjectPayload), "some-bearer-token"), + assertHttpResponseCode(201)); + } + + @Test + @GerritConfig(name = "container.replica", value = "false") + @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token") + public void shouldAcceptPayloadWhenNodeIsAPrimaryWithBearerToken() throws Exception { + url = getURLWithoutAuthenticationPrefix(project.get()); + String payloadWithoutAsyncFieldTemplate = + "{\"label\":\"" + + TEST_REPLICATION_REMOTE + + "\",\"ref_name\":\"%s\",\"revision_data\":{\"commit_object\":{\"type\":1,\"content\":\"%s\"},\"tree_object\":{\"type\":2,\"content\":\"%s\"},\"blobs\":[]}}"; + + String refName = createRef(); + Optional<RevisionData> revisionDataOption = createRevisionData(refName); + assertThat(revisionDataOption.isPresent()).isTrue(); + + RevisionData revisionData = revisionDataOption.get(); + String sendObjectPayload = + createPayload(payloadWithoutAsyncFieldTemplate, refName, revisionData); + + httpClientFactory + .create(source) + .execute( + withBearerTokenAuthentication(createRequest(sendObjectPayload), "some-bearer-token"), + assertHttpResponseCode(201)); + } + private String createPayload( String wrongPayloadTemplate, String refName, RevisionData revisionData) { String sendObjectPayload = @@ -192,7 +242,7 @@ } @Override - protected String getURL(String projectName) { + protected String getURLWithAuthenticationPrefix(String projectName) { return String.format( "%s/a/projects/%s/pull-replication~apply-object", adminRestSession.url(), Url.encode(projectName));
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java new file mode 100644 index 0000000..bbbe66f --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
@@ -0,0 +1,203 @@ +// Copyright (C) 2022 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.replication.pull.api; + +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.httpd.WebSession; +import com.google.gerrit.server.AccessPath; +import com.google.gerrit.server.PluginUser; +import com.google.gerrit.server.util.ThreadLocalRequestContext; +import com.google.inject.Provider; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class BearerAuthenticationFilterTest { + + @Mock private DynamicItem<WebSession> session; + @Mock private WebSession webSession; + @Mock private Provider<PluginUser> pluginUserProvider; + @Mock private Provider<ThreadLocalRequestContext> threadLocalRequestContextProvider; + @Mock private PluginUser pluginUser; + @Mock private ThreadLocalRequestContext threadLocalRequestContext; + @Mock private HttpServletRequest httpServletRequest; + @Mock private HttpServletResponse httpServletResponse; + @Mock private FilterChain filterChain; + private final String pluginName = "pull-replication"; + + private void authenticateWithURI(String uri) throws ServletException, IOException { + final String bearerToken = "some-bearer-token"; + when(httpServletRequest.getRequestURI()).thenReturn(uri); + when(httpServletRequest.getHeader("Authorization")) + .thenReturn(String.format("Bearer %s", bearerToken)); + when(pluginUserProvider.get()).thenReturn(pluginUser); + when(threadLocalRequestContextProvider.get()).thenReturn(threadLocalRequestContext); + when(session.get()).thenReturn(webSession); + final BearerAuthenticationFilter filter = + new BearerAuthenticationFilter( + session, + pluginName, + pluginUserProvider, + threadLocalRequestContextProvider, + bearerToken); + filter.doFilter(httpServletRequest, httpServletResponse, filterChain); + + verify(httpServletRequest).getRequestURI(); + verify(httpServletRequest).getHeader("Authorization"); + verify(pluginUserProvider).get(); + verify(threadLocalRequestContextProvider).get(); + verify(session).get(); + verify(webSession).setAccessPathOk(AccessPath.REST_API, true); + verify(filterChain).doFilter(httpServletRequest, httpServletResponse); + } + + @Test + public void shouldAuthenticateWithBearerTokenWhenFetch() throws ServletException, IOException { + authenticateWithURI("any-prefix/pull-replication~fetch"); + } + + @Test + public void shouldAuthenticateWithBearerTokenWhenApplyObject() + throws ServletException, IOException { + authenticateWithURI("any-prefix/pull-replication~apply-object"); + } + + @Test + public void shouldAuthenticateWithBearerTokenWhenApplyObjects() + throws ServletException, IOException { + authenticateWithURI("any-prefix/pull-replication~apply-objects"); + } + + @Test + public void shouldAuthenticateWithBearerTokenWhenDeleteProject() + throws ServletException, IOException { + authenticateWithURI("any-prefix/pull-replication~delete-project"); + } + + @Test + public void shouldAuthenticateWithBearerTokenWhenUpdateHead() + throws ServletException, IOException { + authenticateWithURI("any-prefix/projects/my-project/HEAD"); + } + + @Test + public void shouldAuthenticateWithBearerTokenWhenInitProject() + throws ServletException, IOException { + authenticateWithURI("any-prefix/pull-replication/init-project/my-project.git"); + } + + @Test + public void shouldBe401WhenBearerTokenDoesNotMatch() throws ServletException, IOException { + when(httpServletRequest.getRequestURI()).thenReturn("any-prefix/pull-replication~fetch"); + when(httpServletRequest.getHeader("Authorization")) + .thenReturn(String.format("Bearer %s", "some-different-bearer-token")); + + final BearerAuthenticationFilter filter = + new BearerAuthenticationFilter( + session, + pluginName, + pluginUserProvider, + threadLocalRequestContextProvider, + "some-bearer-token"); + filter.doFilter(httpServletRequest, httpServletResponse, filterChain); + + verify(httpServletRequest).getRequestURI(); + verify(httpServletRequest).getHeader("Authorization"); + verify(httpServletResponse).sendError(SC_UNAUTHORIZED); + } + + @Test + public void shouldBe401WhenBearerTokenCannotBeExtracted() throws ServletException, IOException { + when(httpServletRequest.getRequestURI()).thenReturn("any-prefix/pull-replication~fetch"); + when(httpServletRequest.getHeader("Authorization")).thenReturn("bearer token"); + + final BearerAuthenticationFilter filter = + new BearerAuthenticationFilter( + session, + pluginName, + pluginUserProvider, + threadLocalRequestContextProvider, + "some-bearer-token"); + filter.doFilter(httpServletRequest, httpServletResponse, filterChain); + + verify(httpServletRequest).getRequestURI(); + verify(httpServletRequest).getHeader("Authorization"); + verify(httpServletResponse).sendError(SC_UNAUTHORIZED); + } + + @Test + public void shouldBe401WhenNoAuthorizationHeaderInRequest() throws ServletException, IOException { + when(httpServletRequest.getRequestURI()).thenReturn("any-prefix/pull-replication~fetch"); + + final BearerAuthenticationFilter filter = + new BearerAuthenticationFilter( + session, + pluginName, + pluginUserProvider, + threadLocalRequestContextProvider, + "some-bearer-token"); + filter.doFilter(httpServletRequest, httpServletResponse, filterChain); + + verify(httpServletRequest).getRequestURI(); + verify(httpServletResponse).sendError(SC_UNAUTHORIZED); + } + + @Test + public void shouldGoNextInChainWhenUriDoesNotMatch() throws ServletException, IOException { + when(httpServletRequest.getRequestURI()).thenReturn("any-url"); + + final BearerAuthenticationFilter filter = + new BearerAuthenticationFilter( + session, + pluginName, + pluginUserProvider, + threadLocalRequestContextProvider, + "some-bearer-token"); + filter.doFilter(httpServletRequest, httpServletResponse, filterChain); + + verify(httpServletRequest).getRequestURI(); + verify(filterChain).doFilter(httpServletRequest, httpServletResponse); + } + + @Test + public void shouldGoNextInChainWhenBasicAuthorizationIsRequired() + throws ServletException, IOException { + when(httpServletRequest.getRequestURI()) + .thenReturn("/a/projects/my-project/pull-replication~fetch"); + + final BearerAuthenticationFilter filter = + new BearerAuthenticationFilter( + session, + pluginName, + pluginUserProvider, + threadLocalRequestContextProvider, + "some-bearer-token"); + filter.doFilter(httpServletRequest, httpServletResponse, filterChain); + + verify(httpServletRequest).getRequestURI(); + verify(filterChain).doFilter(httpServletRequest, httpServletResponse); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java index 5634b2a..cc01c2a 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionIT.java
@@ -78,8 +78,48 @@ .execute(createRequest(sendObjectPayload), assertHttpResponseCode(403)); } + @Test + @GerritConfig(name = "container.replica", value = "true") + @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token") + public void shouldFetchRefWhenNodeIsAReplicaWithBearerToken() throws Exception { + String refName = createRef(); + url = getURLWithoutAuthenticationPrefix(project.get()); + String sendObjectPayload = + "{\"label\":\"" + + TEST_REPLICATION_REMOTE + + "\", \"ref_name\": \"" + + refName + + "\", \"async\":false}"; + + httpClientFactory + .create(source) + .execute( + withBearerTokenAuthentication(createRequest(sendObjectPayload), "some-bearer-token"), + assertHttpResponseCode(201)); + } + + @Test + @GerritConfig(name = "container.replica", value = "false") + @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token") + public void shouldFetchRefWhenNodeIsAPrimaryWithBearerToken() throws Exception { + String refName = createRef(); + url = getURLWithoutAuthenticationPrefix(project.get()); + String sendObjectPayload = + "{\"label\":\"" + + TEST_REPLICATION_REMOTE + + "\", \"ref_name\": \"" + + refName + + "\", \"async\":false}"; + + httpClientFactory + .create(source) + .execute( + withBearerTokenAuthentication(createRequest(sendObjectPayload), "some-bearer-token"), + assertHttpResponseCode(201)); + } + @Override - protected String getURL(String projectName) { + protected String getURLWithAuthenticationPrefix(String projectName) { return String.format( "%s/a/projects/%s/pull-replication~fetch", adminRestSession.url(), Url.encode(projectName)); }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java index 2f61cff..2953ed0 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java
@@ -42,7 +42,7 @@ @Test public void shouldDeleteRepositoryWhenUserHasProjectDeletionCapabilities() throws Exception { String testProjectName = project.get(); - url = getURL(testProjectName); + url = getURLWithAuthenticationPrefix(testProjectName); httpClientFactory .create(source) .execute( @@ -65,7 +65,7 @@ @Test public void shouldReturnOKWhenProjectIsDeleted() throws Exception { String testProjectName = project.get(); - url = getURL(testProjectName); + url = getURLWithAuthenticationPrefix(testProjectName); httpClientFactory .create(source) @@ -76,7 +76,7 @@ @Test public void shouldReturnInternalServerErrorIfProjectCannotBeDeleted() throws Exception { - url = getURL(INVALID_TEST_PROJECT_NAME); + url = getURLWithAuthenticationPrefix(INVALID_TEST_PROJECT_NAME); httpClientFactory .create(source) @@ -97,7 +97,7 @@ @GerritConfig(name = "container.replica", value = "true") public void shouldReturnOKWhenProjectIsDeletedOnReplica() throws Exception { String testProjectName = project.get(); - url = getURL(testProjectName); + url = getURLWithAuthenticationPrefix(testProjectName); httpClientFactory .create(source) @@ -111,7 +111,7 @@ public void shouldDeleteRepositoryWhenUserHasProjectDeletionCapabilitiesAndNodeIsAReplica() throws Exception { String testProjectName = project.get(); - url = getURL(testProjectName); + url = getURLWithAuthenticationPrefix(testProjectName); HttpRequestBase deleteRequest = withBasicAuthenticationAsUser(createDeleteRequest()); httpClientFactory @@ -133,7 +133,7 @@ @GerritConfig(name = "container.replica", value = "true") public void shouldReturnInternalServerErrorIfProjectCannotBeDeletedWhenNodeIsAReplica() throws Exception { - url = getURL(INVALID_TEST_PROJECT_NAME); + url = getURLWithAuthenticationPrefix(INVALID_TEST_PROJECT_NAME); httpClientFactory .create(source) @@ -142,8 +142,36 @@ assertHttpResponseCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)); } + @Test + @GerritConfig(name = "container.replica", value = "true") + @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token") + public void shouldReturnOKWhenProjectIsDeletedOnReplicaWithBearerToken() throws Exception { + String testProjectName = project.get(); + url = getURLWithoutAuthenticationPrefix(testProjectName); + + httpClientFactory + .create(source) + .execute( + withBearerTokenAuthentication(createDeleteRequest(), "some-bearer-token"), + assertHttpResponseCode(HttpServletResponse.SC_OK)); + } + + @Test + @GerritConfig(name = "container.replica", value = "false") + @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token") + public void shouldReturnOKWhenProjectIsDeletedOnPrimaryWithBearerToken() throws Exception { + String testProjectName = project.get(); + url = getURLWithoutAuthenticationPrefix(testProjectName); + + httpClientFactory + .create(source) + .execute( + withBearerTokenAuthentication(createDeleteRequest(), "some-bearer-token"), + assertHttpResponseCode(HttpServletResponse.SC_OK)); + } + @Override - protected String getURL(String projectName) { + protected String getURLWithAuthenticationPrefix(String projectName) { return String.format( "%s/a/projects/%s/pull-replication~delete-project", adminRestSession.url(), Url.encode(projectName));
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java index d1242a0..99b3336 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationActionIT.java
@@ -15,7 +15,6 @@ package com.googlesource.gerrit.plugins.replication.pull.api; import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability; -import static com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction.getProjectInitializationUrl; import com.google.common.net.MediaType; import com.google.gerrit.acceptance.config.GerritConfig; @@ -56,7 +55,7 @@ @Test public void shouldCreateRepository() throws Exception { String newProjectName = "new/newProjectForPrimary"; - url = getURL(newProjectName); + url = getURLWithAuthenticationPrefix(newProjectName); httpClientFactory .create(source) .execute( @@ -75,7 +74,7 @@ @Test public void shouldCreateRepositoryWhenUserHasProjectCreationCapabilities() throws Exception { String newProjectName = "new/newProjectForUserWithCapabilities"; - url = getURL(newProjectName); + url = getURLWithAuthenticationPrefix(newProjectName); HttpRequestBase put = withBasicAuthenticationAsUser(createPutRequestWithHeaders()); httpClientFactory .create(source) @@ -107,7 +106,7 @@ @GerritConfig(name = "container.replica", value = "true") public void shouldCreateRepositoryWhenNodeIsAReplica() throws Exception { String newProjectName = "new/newProjectForReplica"; - url = getURL(newProjectName); + url = getURLWithAuthenticationPrefix(newProjectName); httpClientFactory .create(source) .execute( @@ -130,7 +129,7 @@ public void shouldCreateRepositoryWhenUserHasProjectCreationCapabilitiesAndNodeIsAReplica() throws Exception { String newProjectName = "new/newProjectForUserWithCapabilitiesReplica"; - url = getURL(newProjectName); + url = getURLWithAuthenticationPrefix(newProjectName); HttpRequestBase put = withBasicAuthenticationAsUser(createPutRequestWithHeaders()); httpClientFactory .create(source) @@ -153,7 +152,7 @@ @GerritConfig(name = "container.replica", value = "true") public void shouldReturnInternalServerErrorIfProjectCannotBeCreatedWhenNodeIsAReplica() throws Exception { - url = getURL(INVALID_TEST_PROJECT_NAME); + url = getURLWithAuthenticationPrefix(INVALID_TEST_PROJECT_NAME); httpClientFactory .create(source) .execute( @@ -181,11 +180,36 @@ assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN)); } + @Test + @GerritConfig(name = "container.replica", value = "true") + @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token") + public void shouldCreateRepositoryWhenNodeIsAReplicaWithBearerToken() throws Exception { + String newProjectName = "new/newProjectForReplica"; + url = getURLWithoutAuthenticationPrefix(newProjectName); + httpClientFactory + .create(source) + .execute( + withBearerTokenAuthentication(createPutRequestWithHeaders(), "some-bearer-token"), + assertHttpResponseCode(HttpServletResponse.SC_CREATED)); + } + + @Test + @GerritConfig(name = "container.replica", value = "false") + @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token") + public void shouldCreateRepositoryWhenNodeIsAPrimaryWithBearerToken() throws Exception { + String newProjectName = "new/newProjectForReplica"; + url = getURLWithoutAuthenticationPrefix(newProjectName); + httpClientFactory + .create(source) + .execute( + withBearerTokenAuthentication(createPutRequestWithHeaders(), "some-bearer-token"), + assertHttpResponseCode(HttpServletResponse.SC_CREATED)); + } + @Override - protected String getURL(String projectName) { + protected String getURLWithAuthenticationPrefix(String projectName) { return userRestSession.url() - + "/" - + getProjectInitializationUrl("pull-replication", Url.encode(projectName)); + + String.format("/a/plugins/pull-replication/init-project/%s.git", Url.encode(projectName)); } protected HttpPut createPutRequestWithHeaders() {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadActionIT.java index 5355251..7c725b3 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadActionIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadActionIT.java
@@ -27,6 +27,7 @@ import com.google.inject.Inject; import javax.servlet.http.HttpServletResponse; import org.apache.http.client.methods.HttpRequestBase; +import org.junit.Ignore; import org.junit.Test; public class UpdateHeadActionIT extends ActionITBase { @@ -140,6 +141,50 @@ assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN)); } + @Test + @GerritConfig(name = "container.replica", value = "true") + @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token") + @Ignore("Waiting for resolving: Issue 16332: Not able to update the HEAD from internal user") + public void shouldReturnOKWhenHeadIsUpdatedInReplicaWithBearerToken() throws Exception { + String testProjectName = project.get(); + url = getURLWithoutAuthenticationPrefix(testProjectName); + String newBranch = "refs/heads/mybranch"; + String master = "refs/heads/master"; + BranchInput input = new BranchInput(); + input.revision = master; + gApi.projects().name(testProjectName).branch(newBranch).create(input); + httpClientFactory + .create(source) + .execute( + withBearerTokenAuthentication( + createPutRequest(headInput(newBranch)), "some-bearer-token"), + assertHttpResponseCode(HttpServletResponse.SC_OK)); + + assertThat(gApi.projects().name(testProjectName).head()).isEqualTo(newBranch); + } + + @Test + @GerritConfig(name = "container.replica", value = "false") + @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token") + @Ignore("Waiting for resolving: Issue 16332: Not able to update the HEAD from internal user") + public void shouldReturnOKWhenHeadIsUpdatedInPrimaryWithBearerToken() throws Exception { + String testProjectName = project.get(); + url = getURLWithoutAuthenticationPrefix(testProjectName); + String newBranch = "refs/heads/mybranch"; + String master = "refs/heads/master"; + BranchInput input = new BranchInput(); + input.revision = master; + gApi.projects().name(testProjectName).branch(newBranch).create(input); + httpClientFactory + .create(source) + .execute( + withBearerTokenAuthentication( + createPutRequest(headInput(newBranch)), "some-bearer-token"), + assertHttpResponseCode(HttpServletResponse.SC_OK)); + + assertThat(gApi.projects().name(testProjectName).head()).isEqualTo(newBranch); + } + private String headInput(String ref) { HeadInput headInput = new HeadInput(); headInput.ref = ref; @@ -147,7 +192,7 @@ } @Override - protected String getURL(String projectName) { + protected String getURLWithAuthenticationPrefix(String projectName) { return String.format("%s/a/projects/%s/HEAD", adminRestSession.url(), projectName); } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java similarity index 85% rename from src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java rename to src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java index a6886f1..a2389d7 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
@@ -16,9 +16,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.testing.GerritJUnit.assertThrows; -import static javax.servlet.http.HttpServletResponse.SC_CREATED; import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -30,6 +28,7 @@ import com.google.gerrit.entities.RefNames; import com.googlesource.gerrit.plugins.replication.CredentialsFactory; import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig; +import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider; import com.googlesource.gerrit.plugins.replication.pull.Source; import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData; import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData; @@ -39,32 +38,25 @@ import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.util.Collections; -import java.util.Optional; import org.apache.http.Header; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.message.BasicHeader; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.storage.file.FileBasedConfig; -import org.eclipse.jgit.transport.CredentialItem; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; -import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; -@RunWith(MockitoJUnitRunner.class) -public class FetchRestApiClientTest { +public abstract class FetchRestApiClientBase { private static final boolean IS_REF_UPDATE = false; @Mock CredentialsProvider credentialProvider; @@ -74,10 +66,10 @@ @Mock FileBasedConfig config; @Mock ReplicationFileBasedConfig replicationConfig; @Mock Source source; + @Mock BearerTokenProvider bearerTokenProvider; @Captor ArgumentCaptor<HttpPost> httpPostCaptor; @Captor ArgumentCaptor<HttpPut> httpPutCaptor; @Captor ArgumentCaptor<HttpDelete> httpDeleteCaptor; - String api = "http://gerrit-host"; String pluginName = "pull-replication"; String instanceId = "Replication"; @@ -150,43 +142,9 @@ FetchApiClient objectUnderTest; - @Before - public void setup() throws ClientProtocolException, IOException { - when(credentialProvider.supports(any())) - .thenAnswer( - new Answer<Boolean>() { + protected abstract String urlAuthenticationPrefix(); - @Override - public Boolean answer(InvocationOnMock invocation) throws Throwable { - CredentialItem.Username user = (CredentialItem.Username) invocation.getArgument(0); - CredentialItem.Password password = - (CredentialItem.Password) invocation.getArgument(1); - user.setValue("admin"); - password.setValue("secret".toCharArray()); - return true; - } - }); - - when(credentialProvider.get(any(), any(CredentialItem.class))).thenReturn(true); - when(credentials.create(anyString())).thenReturn(credentialProvider); - when(replicationConfig.getConfig()).thenReturn(config); - when(config.getStringList("replication", null, "syncRefs")).thenReturn(new String[0]); - when(source.getRemoteConfigName()).thenReturn("Replication"); - - HttpResult httpResult = new HttpResult(SC_CREATED, Optional.of("result message")); - when(httpClient.execute(any(HttpPost.class), any())).thenReturn(httpResult); - when(httpClientFactory.create(any())).thenReturn(httpClient); - syncRefsFilter = new SyncRefsFilter(replicationConfig); - objectUnderTest = - new FetchRestApiClient( - credentials, - httpClientFactory, - replicationConfig, - syncRefsFilter, - pluginName, - instanceId, - source); - } + protected abstract void assertAuthentication(HttpRequestBase httpRequest); @Test public void shouldCallFetchEndpoint() @@ -199,24 +157,16 @@ HttpPost httpPost = httpPostCaptor.getValue(); assertThat(httpPost.getURI().getHost()).isEqualTo("gerrit-host"); assertThat(httpPost.getURI().getPath()) - .isEqualTo("/a/projects/test_repo/pull-replication~fetch"); + .isEqualTo( + String.format( + "%s/projects/test_repo/pull-replication~fetch", urlAuthenticationPrefix())); + assertAuthentication(httpPost); } @Test public void shouldByDefaultCallSyncFetchForAllRefs() throws ClientProtocolException, IOException, URISyntaxException { - syncRefsFilter = new SyncRefsFilter(replicationConfig); - objectUnderTest = - new FetchRestApiClient( - credentials, - httpClientFactory, - replicationConfig, - syncRefsFilter, - pluginName, - instanceId, - source); - objectUnderTest.callFetch(Project.nameKey("test_repo"), refName, new URIish(api)); verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any()); @@ -240,6 +190,7 @@ syncRefsFilter, pluginName, instanceId, + bearerTokenProvider, source); objectUnderTest.callFetch(Project.nameKey("test_repo"), refName, new URIish(api)); @@ -268,6 +219,7 @@ syncRefsFilter, pluginName, instanceId, + bearerTokenProvider, source); objectUnderTest.callFetch(Project.nameKey("test_repo"), refName, new URIish(api)); @@ -322,7 +274,10 @@ HttpPost httpPost = httpPostCaptor.getValue(); assertThat(httpPost.getURI().getHost()).isEqualTo("gerrit-host"); assertThat(httpPost.getURI().getPath()) - .isEqualTo("/a/projects/test_repo/pull-replication~apply-object"); + .isEqualTo( + String.format( + "%s/projects/test_repo/pull-replication~apply-object", urlAuthenticationPrefix())); + assertAuthentication(httpPost); } @Test @@ -367,6 +322,7 @@ syncRefsFilter, pluginName, null, + bearerTokenProvider, source)); } @@ -382,6 +338,7 @@ syncRefsFilter, pluginName, " ", + bearerTokenProvider, source)); } @@ -397,6 +354,7 @@ syncRefsFilter, pluginName, "", + bearerTokenProvider, source)); } @@ -412,6 +370,7 @@ syncRefsFilter, pluginName, "", + bearerTokenProvider, source); objectUnderTest.callFetch(Project.nameKey("test_repo"), refName, new URIish(api)); @@ -431,7 +390,11 @@ HttpPut httpPut = httpPutCaptor.getValue(); assertThat(httpPut.getURI().getHost()).isEqualTo("gerrit-host"); assertThat(httpPut.getURI().getPath()) - .isEqualTo("/a/plugins/pull-replication/init-project/test_repo.git"); + .isEqualTo( + String.format( + "%s/plugins/pull-replication/init-project/test_repo.git", + urlAuthenticationPrefix())); + assertAuthentication(httpPut); } @Test @@ -444,7 +407,11 @@ HttpDelete httpDelete = httpDeleteCaptor.getValue(); assertThat(httpDelete.getURI().getHost()).isEqualTo("gerrit-host"); assertThat(httpDelete.getURI().getPath()) - .isEqualTo("/a/projects/test_repo/pull-replication~delete-project"); + .isEqualTo( + String.format( + "%s/projects/test_repo/pull-replication~delete-project", + urlAuthenticationPrefix())); + assertAuthentication(httpDelete); } @Test @@ -462,8 +429,11 @@ assertThat(httpPut.getURI().getHost()).isEqualTo("gerrit-host"); assertThat(httpPut.getURI().getPath()) - .isEqualTo(String.format("/a/projects/%s/pull-replication~HEAD", projectName)); + .isEqualTo( + String.format( + "%s/projects/%s/pull-replication~HEAD", urlAuthenticationPrefix(), projectName)); assertThat(payload).isEqualTo(String.format("{\"ref\": \"%s\"}", newHead)); + assertAuthentication(httpPut); } public String readPayload(HttpPost entity) throws UnsupportedOperationException, IOException {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientWithBasicAuthenticationTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientWithBasicAuthenticationTest.java new file mode 100644 index 0000000..644afce --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientWithBasicAuthenticationTest.java
@@ -0,0 +1,90 @@ +// Copyright (C) 2022 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.replication.pull.client; + +import static com.google.common.truth.Truth.assertThat; +import static javax.servlet.http.HttpServletResponse.SC_CREATED; +import static org.mockito.Mockito.*; + +import com.googlesource.gerrit.plugins.replication.pull.filter.SyncRefsFilter; +import java.io.IOException; +import java.util.Optional; +import org.apache.http.Header; +import org.apache.http.HttpHeaders; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpRequestBase; +import org.eclipse.jgit.transport.CredentialItem; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +@RunWith(MockitoJUnitRunner.class) +public class FetchRestApiClientWithBasicAuthenticationTest extends FetchRestApiClientBase { + + @Before + public void setup() throws ClientProtocolException, IOException { + when(bearerTokenProvider.get()).thenReturn(Optional.empty()); + when(credentialProvider.supports(any())) + .thenAnswer( + new Answer<Boolean>() { + + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + CredentialItem.Username user = (CredentialItem.Username) invocation.getArgument(0); + CredentialItem.Password password = + (CredentialItem.Password) invocation.getArgument(1); + user.setValue("admin"); + password.setValue("secret".toCharArray()); + return true; + } + }); + + when(credentialProvider.get(any(), any(CredentialItem.class))).thenReturn(true); + when(credentials.create(anyString())).thenReturn(credentialProvider); + when(replicationConfig.getConfig()).thenReturn(config); + when(config.getStringList("replication", null, "syncRefs")).thenReturn(new String[0]); + when(source.getRemoteConfigName()).thenReturn("Replication"); + + HttpResult httpResult = new HttpResult(SC_CREATED, Optional.of("result message")); + when(httpClient.execute(any(HttpRequestBase.class), any())).thenReturn(httpResult); + when(httpClientFactory.create(any())).thenReturn(httpClient); + syncRefsFilter = new SyncRefsFilter(replicationConfig); + objectUnderTest = + new FetchRestApiClient( + credentials, + httpClientFactory, + replicationConfig, + syncRefsFilter, + pluginName, + instanceId, + bearerTokenProvider, + source); + verify(bearerTokenProvider).get(); + } + + @Override + protected String urlAuthenticationPrefix() { + return "/a"; + } + + @Override + protected void assertAuthentication(HttpRequestBase httpRequest) { + Header[] authorizationHeaders = httpRequest.getHeaders(HttpHeaders.AUTHORIZATION); + assertThat(authorizationHeaders.length).isEqualTo(1); + assertThat(authorizationHeaders[0].getValue()).contains("Basic"); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientWithBearerTokenTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientWithBearerTokenTest.java new file mode 100644 index 0000000..90d71ad --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientWithBearerTokenTest.java
@@ -0,0 +1,70 @@ +// Copyright (C) 2022 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.replication.pull.client; + +import static com.google.common.truth.Truth.assertThat; +import static javax.servlet.http.HttpServletResponse.SC_CREATED; +import static org.mockito.Mockito.*; + +import com.googlesource.gerrit.plugins.replication.pull.filter.SyncRefsFilter; +import java.io.IOException; +import java.util.Optional; +import org.apache.http.Header; +import org.apache.http.HttpHeaders; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpRequestBase; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class FetchRestApiClientWithBearerTokenTest extends FetchRestApiClientBase { + + @Before + public void setup() throws ClientProtocolException, IOException { + when(bearerTokenProvider.get()).thenReturn(Optional.of("some-bearer-token")); + when(replicationConfig.getConfig()).thenReturn(config); + when(config.getStringList("replication", null, "syncRefs")).thenReturn(new String[0]); + HttpResult httpResult = new HttpResult(SC_CREATED, Optional.of("result message")); + when(httpClient.execute(any(HttpRequestBase.class), any())).thenReturn(httpResult); + when(httpClientFactory.create(any())).thenReturn(httpClient); + + syncRefsFilter = new SyncRefsFilter(replicationConfig); + + objectUnderTest = + new FetchRestApiClient( + credentials, + httpClientFactory, + replicationConfig, + syncRefsFilter, + pluginName, + instanceId, + bearerTokenProvider, + source); + verify(bearerTokenProvider).get(); + } + + @Override + protected String urlAuthenticationPrefix() { + return ""; + } + + @Override + protected void assertAuthentication(HttpRequestBase httpRequest) { + Header[] authorizationHeaders = httpRequest.getHeaders(HttpHeaders.AUTHORIZATION); + assertThat(authorizationHeaders.length).isEqualTo(1); + assertThat(authorizationHeaders[0].getValue()).isEqualTo("Bearer " + "some-bearer-token"); + } +}