Merge branch 'stable-3.3' into stable-3.4 * stable-3.3: Return 400 when deleting/creating an invalid project name Change-Id: Ic51d36acf6e884acff15cd78b1efbecf61c17f83
diff --git a/BUILD b/BUILD index 82112d1..5c5f240 100644 --- a/BUILD +++ b/BUILD
@@ -46,6 +46,6 @@ ), deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [ ":pull-replication__plugin", - "//plugins/replication:replication", + "//plugins/replication:replication", ], )
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/FetchOne.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java index cdf573f..4fad8f9 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
@@ -66,7 +66,7 @@ */ public class FetchOne implements ProjectRunnable, CanceledWhileRunning { private final ReplicationStateListener stateLog; - static final String ALL_REFS = "..all.."; + public static final String ALL_REFS = "..all.."; static final String ID_KEY = "fetchOneId"; interface Factory {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/LocalGitRepositoryManagerProvider.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/LocalGitRepositoryManagerProvider.java new file mode 100644 index 0000000..ea8aa00 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/LocalGitRepositoryManagerProvider.java
@@ -0,0 +1,47 @@ +// 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.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.LocalDiskRepositoryManager; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.google.inject.name.Named; + +@Singleton +public class LocalGitRepositoryManagerProvider implements Provider<GitRepositoryManager> { + + @Inject(optional = true) + @Named("LocalDiskRepositoryManager") + @Nullable + private GitRepositoryManager gitRepositoryManager; + + private final LocalDiskRepositoryManager localDiskRepositoryManager; + + @VisibleForTesting + @Inject + public LocalGitRepositoryManagerProvider(LocalDiskRepositoryManager localDiskRepositoryManager) { + this.localDiskRepositoryManager = localDiskRepositoryManager; + } + + @Override + public GitRepositoryManager get() { + return MoreObjects.firstNonNull(gitRepositoryManager, localDiskRepositoryManager); + } +}
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 c17d5df..1671410 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
@@ -42,18 +42,22 @@ import com.googlesource.gerrit.plugins.replication.ReplicationConfig; import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig; import com.googlesource.gerrit.plugins.replication.StartReplicationCapability; +import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob; import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiModule; +import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupModule; import com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient; import com.googlesource.gerrit.plugins.replication.pull.client.FetchRestApiClient; import com.googlesource.gerrit.plugins.replication.pull.client.HttpClient; import com.googlesource.gerrit.plugins.replication.pull.client.SourceHttpClient; import com.googlesource.gerrit.plugins.replication.pull.event.FetchRefReplicatedEventModule; +import com.googlesource.gerrit.plugins.replication.pull.event.StreamEventModule; import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.util.FS; @@ -70,9 +74,11 @@ @Override protected void configure() { + install(new PullReplicationGroupModule()); + bind(BearerTokenProvider.class).in(Scopes.SINGLETON); bind(RevisionReader.class).in(Scopes.SINGLETON); bind(ApplyObject.class); - + install(new FactoryModuleBuilder().build(FetchJob.Factory.class)); install(new PullReplicationApiModule()); install(new FetchRefReplicatedEventModule()); @@ -120,7 +126,8 @@ bind(ConfigParser.class).to(SourceConfigParser.class).in(Scopes.SINGLETON); - if (getReplicationConfig().getBoolean("gerrit", "autoReload", false)) { + Config replicationConfig = getReplicationConfig(); + if (replicationConfig.getBoolean("gerrit", "autoReload", false)) { bind(ReplicationConfig.class) .annotatedWith(MainReplicationConfig.class) .to(getReplicationConfigClass()); @@ -132,6 +139,10 @@ bind(ReplicationConfig.class).to(getReplicationConfigClass()).in(Scopes.SINGLETON); } + if (replicationConfig.getBoolean("replication", "consumeStreamEvents", false)) { + install(new StreamEventModule()); + } + DynamicSet.setOf(binder(), ReplicationStateListener.class); DynamicSet.bind(binder(), ReplicationStateListener.class).to(PullReplicationStateLogger.class); EventTypes.register(FetchRefReplicatedEvent.TYPE, FetchRefReplicatedEvent.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..8147149 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
@@ -0,0 +1,140 @@ +// 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.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 com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationInternalUser; +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 PullReplicationInternalUser pluginUser; + 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, + PullReplicationInternalUser pluginUser, + Provider<ThreadLocalRequestContext> threadLocalRequestContext, + @Named(BEARER_TOKEN) String bearerToken) { + this.session = session; + this.pluginName = pluginName; + this.pluginUser = pluginUser; + 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) || isGitUploadPackRequest(httpRequest)) { + Optional<String> authorizationHeader = + Optional.ofNullable(httpRequest.getHeader("Authorization")); + + if (isBearerTokenAuthenticated(authorizationHeader, bearerToken)) + try (ManualRequestContext ctx = + new ManualRequestContext(pluginUser, 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 isGitUploadPackRequest(HttpServletRequest requestURI) { + return requestURI.getRequestURI().contains("git-upload-pack") + || Optional.ofNullable(requestURI.getQueryString()) + .map(q -> q.contains("git-upload-pack")) + .orElse(false); + } + + 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/DeleteRefCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java index 2a3a79d..40e03f1 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
@@ -22,7 +22,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.server.events.EventDispatcher; -import com.google.gerrit.server.git.LocalDiskRepositoryManager; +import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.permissions.RefPermission; @@ -31,6 +31,7 @@ import com.google.inject.Inject; import com.googlesource.gerrit.plugins.replication.pull.Context; import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent; +import com.googlesource.gerrit.plugins.replication.pull.LocalGitRepositoryManagerProvider; import com.googlesource.gerrit.plugins.replication.pull.PullReplicationStateLogger; import com.googlesource.gerrit.plugins.replication.pull.ReplicationState; import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject; @@ -49,7 +50,7 @@ private final DynamicItem<EventDispatcher> eventDispatcher; private final ProjectCache projectCache; private final PermissionBackend permissionBackend; - private final LocalDiskRepositoryManager gitManager; + private final GitRepositoryManager gitManager; @Inject public DeleteRefCommand( @@ -58,13 +59,13 @@ ApplyObject applyObject, PermissionBackend permissionBackend, DynamicItem<EventDispatcher> eventDispatcher, - LocalDiskRepositoryManager gitManager) { + LocalGitRepositoryManagerProvider gitManagerProvider) { this.fetchStateLog = fetchStateLog; this.projectCache = projectCache; this.applyObject = applyObject; this.eventDispatcher = eventDispatcher; this.permissionBackend = permissionBackend; - this.gitManager = gitManager; + this.gitManager = gitManagerProvider.get(); } public void deleteRef(Project.NameKey name, String refName, String sourceLabel)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java index 48964db..fdb4f8f 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
@@ -17,7 +17,6 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.base.Strings; -import com.google.common.flogger.FluentLogger; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.AuthException; @@ -32,6 +31,7 @@ import com.google.gerrit.server.project.ProjectResource; import com.google.inject.Inject; import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input; +import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob.Factory; import com.googlesource.gerrit.plugins.replication.pull.api.exception.RemoteConfigurationMissingException; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -42,17 +42,20 @@ private final WorkQueue workQueue; private final DynamicItem<UrlFormatter> urlFormatter; private final FetchPreconditions preConditions; + private final Factory fetchJobFactory; @Inject public FetchAction( FetchCommand command, WorkQueue workQueue, DynamicItem<UrlFormatter> urlFormatter, - FetchPreconditions preConditions) { + FetchPreconditions preConditions, + FetchJob.Factory fetchJobFactory) { this.command = command; this.workQueue = workQueue; this.urlFormatter = urlFormatter; this.preConditions = preConditions; + this.fetchJobFactory = fetchJobFactory; } public static class Input { @@ -104,7 +107,7 @@ workQueue .getDefaultQueue() .submit( - new FetchJob(command, project, input, PullReplicationApiRequestMetrics.get())); + fetchJobFactory.create(project, input, PullReplicationApiRequestMetrics.get())); Optional<String> url = urlFormatter .get() @@ -113,38 +116,4 @@ checkState(url.isPresent()); return Response.accepted(url.get()); } - - private static class FetchJob implements Runnable { - private static final FluentLogger log = FluentLogger.forEnclosingClass(); - - private FetchCommand command; - private Project.NameKey project; - private FetchAction.Input input; - private final PullReplicationApiRequestMetrics apiRequestMetrics; - - public FetchJob( - FetchCommand command, - Project.NameKey project, - FetchAction.Input input, - PullReplicationApiRequestMetrics apiRequestMetrics) { - this.command = command; - this.project = project; - this.input = input; - this.apiRequestMetrics = apiRequestMetrics; - } - - @Override - public void run() { - try { - command.fetchAsync(project, input.label, input.refName, apiRequestMetrics); - } catch (InterruptedException - | ExecutionException - | RemoteConfigurationMissingException - | TimeoutException e) { - log.atSevere().withCause(e).log( - "Exception during the async fetch call for project %s, label %s and ref name %s", - project.get(), input.label, input.refName); - } - } - } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchJob.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchJob.java new file mode 100644 index 0000000..e15dd68 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchJob.java
@@ -0,0 +1,63 @@ +// 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.common.flogger.FluentLogger; +import com.google.gerrit.entities.Project; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import com.googlesource.gerrit.plugins.replication.pull.api.exception.RemoteConfigurationMissingException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +public class FetchJob implements Runnable { + private static final FluentLogger log = FluentLogger.forEnclosingClass(); + + public interface Factory { + FetchJob create( + Project.NameKey project, FetchAction.Input input, PullReplicationApiRequestMetrics metrics); + } + + private FetchCommand command; + private Project.NameKey project; + private FetchAction.Input input; + private final PullReplicationApiRequestMetrics metrics; + + @Inject + public FetchJob( + FetchCommand command, + @Assisted Project.NameKey project, + @Assisted FetchAction.Input input, + @Assisted PullReplicationApiRequestMetrics metrics) { + this.command = command; + this.project = project; + this.input = input; + this.metrics = metrics; + } + + @Override + public void run() { + try { + command.fetchAsync(project, input.label, input.refName, metrics); + } catch (InterruptedException + | ExecutionException + | RemoteConfigurationMissingException + | TimeoutException e) { + log.atSevere().withCause(e).log( + "Exception during the async fetch call for project %s, label %s and ref name %s", + project.get(), input.label, input.refName); + } + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java index ca1557a..161bcf4 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchPreconditions.java
@@ -40,8 +40,10 @@ } public Boolean canCallFetchApi() { - PermissionBackend.WithUser userPermission = permissionBackend.user(userProvider.get()); - return userPermission.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER) + CurrentUser currentUser = userProvider.get(); + PermissionBackend.WithUser userPermission = permissionBackend.user(currentUser); + return currentUser.isInternalUser() + || userPermission.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER) || userPermission.testOrFalse(new PluginPermission(pluginName, CALL_FETCH_ACTION)); } }
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 b140cb4..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,18 +19,36 @@ 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 protected void configureServlets() { + DynamicSet.bind(binder(), AllRequestFilter.class) + .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) @@ -38,9 +56,5 @@ } else { serveRegex("/init-project/.*$").with(ProjectInitializationAction.class); } - - DynamicSet.bind(binder(), AllRequestFilter.class) - .to(PullReplicationApiMetricsFilter.class) - .in(Scopes.SINGLETON); } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java index 8915e78..2e1c5d4 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java
@@ -22,9 +22,11 @@ import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.project.ProjectResource; import com.google.inject.Inject; +import com.google.inject.Provider; import com.googlesource.gerrit.plugins.replication.LocalFS; import com.googlesource.gerrit.plugins.replication.pull.GerritConfigOps; import java.util.Optional; @@ -37,20 +39,29 @@ static class DeleteInput {} + private final Provider<CurrentUser> userProvider; private final GerritConfigOps gerritConfigOps; private final PermissionBackend permissionBackend; @Inject - ProjectDeletionAction(GerritConfigOps gerritConfigOps, PermissionBackend permissionBackend) { + ProjectDeletionAction( + GerritConfigOps gerritConfigOps, + PermissionBackend permissionBackend, + Provider<CurrentUser> userProvider) { this.gerritConfigOps = gerritConfigOps; this.permissionBackend = permissionBackend; + this.userProvider = userProvider; } @Override public Response<?> apply(ProjectResource projectResource, DeleteInput input) throws AuthException, BadRequestException, ResourceConflictException, Exception { - permissionBackend.user(projectResource.getUser()).check(DELETE_PROJECT); + // When triggered internally(for example by consuming stream events) user is not provided + // and internal user is returned. Project deletion should be always allowed for internal user. + if (!userProvider.get().isInternalUser()) { + permissionBackend.user(projectResource.getUser()).check(DELETE_PROJECT); + } Optional<URIish> maybeRepoURI = gerritConfigOps.getGitRepositoryURI(String.format("%s.git", projectResource.getName()));
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 3426c03..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
@@ -14,7 +14,6 @@ package com.googlesource.gerrit.plugins.replication.pull.api; -import static com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability.CALL_FETCH_ACTION; import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.checkAcceptHeader; import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.setResponse; @@ -70,17 +69,8 @@ return; } - if (!userProvider.get().isIdentifiedUser()) { - setResponse( - httpServletResponse, - HttpServletResponse.SC_UNAUTHORIZED, - "Unauthorized user. '" + CALL_FETCH_ACTION + "' capability needed."); - return; - } - String path = httpServletRequest.getRequestURI(); String projectName = Url.decode(path.substring(path.lastIndexOf('/') + 1)); - try { if (initProject(projectName)) { setResponse( @@ -103,10 +93,12 @@ "Cannot initialize project " + projectName); } - protected boolean initProject(String projectName) - throws AuthException, PermissionBackendException { - permissionBackend.user(userProvider.get()).check(GlobalPermission.CREATE_PROJECT); - + public boolean initProject(String projectName) throws AuthException, PermissionBackendException { + // When triggered internally(for example by consuming stream events) user is not provided + // and internal user is returned. Project creation should be always allowed for internal user. + if (!userProvider.get().isInternalUser()) { + permissionBackend.user(userProvider.get()).check(GlobalPermission.CREATE_PROJECT); + } Optional<URIish> maybeUri = gerritConfigOps.getGitRepositoryURI(projectName); if (!maybeUri.isPresent()) { logger.atSevere().log("Cannot initialize project '%s'", projectName); @@ -116,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/api/PullReplicationApiRequestMetrics.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiRequestMetrics.java index 597b66f..8e4e43f 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiRequestMetrics.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiRequestMetrics.java
@@ -16,6 +16,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; +import com.google.gerrit.server.events.Event; import com.google.inject.Inject; import com.googlesource.gerrit.plugins.replication.pull.FetchReplicationMetrics; import java.util.Optional; @@ -28,7 +29,7 @@ public static final String HTTP_HEADER_X_START_TIME_NANOS = "X-StartTimeNanos"; - private Optional<Long> startTimeNanos; + private Optional<Long> startTimeNanos = Optional.empty(); private final AtomicBoolean initialised = new AtomicBoolean(); private final FetchReplicationMetrics metrics; @@ -59,6 +60,13 @@ .map(nanoTime -> Math.min(currentTimeNanos(), nanoTime)); } + public void start(Event event) { + if (!initialised.compareAndSet(false, true)) { + throw new IllegalStateException("PullReplicationApiRequestMetrics already initialised"); + } + startTimeNanos = Optional.of(event.eventCreatedOn * 1000 * 1000 * 1000); + } + public Optional<Long> stop(String replicationSourceName) { return startTimeNanos.map( start -> {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java index 882a74c..20122a7 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
@@ -23,9 +23,7 @@ import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_OK; -import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; -import com.google.common.base.Splitter; import com.google.common.flogger.FluentLogger; import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.api.projects.HeadInput; @@ -38,11 +36,9 @@ import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; -import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.httpd.AllRequestFilter; import com.google.gerrit.httpd.restapi.RestApiServlet; import com.google.gerrit.json.OutputFormat; -import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ProjectResource; import com.google.gerrit.server.restapi.project.ProjectsCollection; @@ -51,7 +47,6 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.MalformedJsonException; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.TypeLiteral; import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input; import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput; @@ -62,9 +57,11 @@ import java.io.IOException; import java.io.PrintWriter; import java.nio.file.InvalidPathException; -import java.util.ArrayList; -import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; +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; @@ -75,6 +72,10 @@ public class PullReplicationFilter extends AllRequestFilter { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final Pattern projectNameInGerritUrl = Pattern.compile(".*/projects/([^/]+)/.*"); + private static final Pattern projectNameInitProjectUrl = + Pattern.compile(".*/init-project/([^/]+.git)"); + private FetchAction fetchAction; private ApplyObjectAction applyObjectAction; private ApplyObjectsAction applyObjectsAction; @@ -83,7 +84,6 @@ private ProjectDeletionAction projectDeletionAction; private ProjectsCollection projectsCollection; private Gson gson; - private Provider<CurrentUser> userProvider; private String pluginName; @Inject @@ -95,7 +95,6 @@ UpdateHeadAction updateHEADAction, ProjectDeletionAction projectDeletionAction, ProjectsCollection projectsCollection, - Provider<CurrentUser> userProvider, @PluginName String pluginName) { this.fetchAction = fetchAction; this.applyObjectAction = applyObjectAction; @@ -104,7 +103,6 @@ this.updateHEADAction = updateHEADAction; this.projectDeletionAction = projectDeletionAction; this.projectsCollection = projectsCollection; - this.userProvider = userProvider; this.pluginName = pluginName; this.gson = OutputFormat.JSON.newGsonBuilder().create(); } @@ -121,44 +119,20 @@ HttpServletRequest httpRequest = (HttpServletRequest) request; try { if (isFetchAction(httpRequest)) { - if (userProvider.get().isIdentifiedUser()) { - writeResponse(httpResponse, doFetch(httpRequest)); - } else { - httpResponse.sendError(SC_UNAUTHORIZED); - } + writeResponse(httpResponse, doFetch(httpRequest)); } else if (isApplyObjectAction(httpRequest)) { - if (userProvider.get().isIdentifiedUser()) { - writeResponse(httpResponse, doApplyObject(httpRequest)); - } else { - httpResponse.sendError(SC_UNAUTHORIZED); - } + writeResponse(httpResponse, doApplyObject(httpRequest)); } else if (isApplyObjectsAction(httpRequest)) { - if (userProvider.get().isIdentifiedUser()) { - writeResponse(httpResponse, doApplyObjects(httpRequest)); - } else { - httpResponse.sendError(SC_UNAUTHORIZED); - } + writeResponse(httpResponse, doApplyObjects(httpRequest)); } else if (isInitProjectAction(httpRequest)) { - if (userProvider.get().isIdentifiedUser()) { - if (!checkAcceptHeader(httpRequest, httpResponse)) { - return; - } - doInitProject(httpRequest, httpResponse); - } else { - httpResponse.sendError(SC_UNAUTHORIZED); + if (!checkAcceptHeader(httpRequest, httpResponse)) { + return; } + doInitProject(httpRequest, httpResponse); } else if (isUpdateHEADAction(httpRequest)) { - if (userProvider.get().isIdentifiedUser()) { - writeResponse(httpResponse, doUpdateHEAD(httpRequest)); - } else { - httpResponse.sendError(SC_UNAUTHORIZED); - } + writeResponse(httpResponse, doUpdateHEAD(httpRequest)); } else if (isDeleteProjectAction(httpRequest)) { - if (userProvider.get().isIdentifiedUser()) { - writeResponse(httpResponse, doDeleteProject(httpRequest)); - } else { - httpResponse.sendError(SC_UNAUTHORIZED); - } + writeResponse(httpResponse, doDeleteProject(httpRequest)); } else { chain.doFilter(request, response); } @@ -181,6 +155,9 @@ } catch (InitProjectException | ResourceNotFoundException e) { RestApiServlet.replyError( httpRequest, httpResponse, SC_INTERNAL_SERVER_ERROR, e.getMessage(), e.caching(), e); + } catch (NoSuchElementException e) { + RestApiServlet.replyError( + httpRequest, httpResponse, SC_BAD_REQUEST, "Project name not present in the url", e); } catch (Exception e) { if (e instanceof InvalidPathException || e.getCause() instanceof InvalidPathException) { RestApiServlet.replyError( @@ -194,8 +171,8 @@ private void doInitProject(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws RestApiException, IOException, PermissionBackendException { - String path = httpRequest.getRequestURI(); - String projectName = Url.decode(path.substring(path.lastIndexOf('/') + 1)); + IdString id = getInitProjectName(httpRequest).get(); + String projectName = id.get(); if (projectInitializationAction.initProject(projectName)) { setResponse( httpResponse, HttpServletResponse.SC_CREATED, "Project " + projectName + " initialized"); @@ -208,7 +185,7 @@ private Response<Map<String, Object>> doApplyObject(HttpServletRequest httpRequest) throws RestApiException, IOException, PermissionBackendException { RevisionInput input = readJson(httpRequest, TypeLiteral.get(RevisionInput.class)); - IdString id = getProjectName(httpRequest); + IdString id = getProjectName(httpRequest).get(); ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); return (Response<Map<String, Object>>) applyObjectAction.apply(projectResource, input); @@ -218,7 +195,7 @@ private Response<Map<String, Object>> doApplyObjects(HttpServletRequest httpRequest) throws RestApiException, IOException, PermissionBackendException { RevisionsInput input = readJson(httpRequest, TypeLiteral.get(RevisionsInput.class)); - IdString id = getProjectName(httpRequest); + IdString id = getProjectName(httpRequest).get(); ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); return (Response<Map<String, Object>>) applyObjectsAction.apply(projectResource, input); @@ -227,16 +204,16 @@ @SuppressWarnings("unchecked") private Response<String> doUpdateHEAD(HttpServletRequest httpRequest) throws Exception { HeadInput input = readJson(httpRequest, TypeLiteral.get(HeadInput.class)); - ProjectResource projectResource = - projectsCollection.parse(TopLevelResource.INSTANCE, getProjectName(httpRequest)); + IdString id = getProjectName(httpRequest).get(); + ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); return (Response<String>) updateHEADAction.apply(projectResource, input); } @SuppressWarnings("unchecked") private Response<String> doDeleteProject(HttpServletRequest httpRequest) throws Exception { - ProjectResource projectResource = - projectsCollection.parse(TopLevelResource.INSTANCE, getProjectName(httpRequest)); + IdString id = getProjectName(httpRequest).get(); + ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); return (Response<String>) projectDeletionAction.apply(projectResource, new ProjectDeletionAction.DeleteInput()); } @@ -245,7 +222,7 @@ private Response<Map<String, Object>> doFetch(HttpServletRequest httpRequest) throws IOException, RestApiException, PermissionBackendException { Input input = readJson(httpRequest, TypeLiteral.get(Input.class)); - IdString id = getProjectName(httpRequest); + IdString id = getProjectName(httpRequest).get(); ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); return (Response<Map<String, Object>>) fetchAction.apply(projectResource, input); @@ -302,42 +279,48 @@ * @param req * @return project name */ - private IdString getProjectName(HttpServletRequest req) { - String path = req.getRequestURI(); + private Optional<IdString> getInitProjectName(HttpServletRequest req) { + return extractProjectName(req, projectNameInitProjectUrl); + } - List<IdString> out = new ArrayList<>(); - for (String p : Splitter.on('/').split(path)) { - out.add(IdString.fromUrl(p)); + private Optional<IdString> getProjectName(HttpServletRequest req) { + return extractProjectName(req, projectNameInGerritUrl); + } + + private Optional<IdString> extractProjectName(HttpServletRequest req, Pattern urlPattern) { + String path = req.getRequestURI(); + Matcher projectGroupMatcher = urlPattern.matcher(path); + + if (projectGroupMatcher.find()) { + return Optional.of(IdString.fromUrl(projectGroupMatcher.group(1))); } - if (!out.isEmpty() && out.get(out.size() - 1).isEmpty()) { - out.remove(out.size() - 1); - } - return out.get(3); + + return Optional.empty(); } private boolean isApplyObjectAction(HttpServletRequest httpRequest) { - return httpRequest.getRequestURI().endsWith("pull-replication~apply-object"); + return httpRequest.getRequestURI().endsWith(String.format("/%s~apply-object", pluginName)); } private boolean isApplyObjectsAction(HttpServletRequest httpRequest) { - return httpRequest.getRequestURI().endsWith("pull-replication~apply-objects"); + return httpRequest.getRequestURI().endsWith(String.format("/%s~apply-objects", pluginName)); } private boolean isFetchAction(HttpServletRequest httpRequest) { - return httpRequest.getRequestURI().endsWith("pull-replication~fetch"); + return httpRequest.getRequestURI().endsWith(String.format("/%s~fetch", pluginName)); } private boolean isInitProjectAction(HttpServletRequest httpRequest) { - return httpRequest.getRequestURI().contains("pull-replication/init-project/"); + return httpRequest.getRequestURI().contains(String.format("/%s/init-project/", pluginName)); } private boolean isUpdateHEADAction(HttpServletRequest httpRequest) { - return httpRequest.getRequestURI().matches("(/a)?/projects/[^/]+/HEAD") + return httpRequest.getRequestURI().matches(".*/projects/[^/]+/HEAD") && "PUT".equals(httpRequest.getMethod()); } private boolean isDeleteProjectAction(HttpServletRequest httpRequest) { - return httpRequest.getRequestURI().endsWith(String.format("%s~delete-project", pluginName)) + return httpRequest.getRequestURI().endsWith(String.format("/%s~delete-project", pluginName)) && "DELETE".equals(httpRequest.getMethod()); } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackend.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackend.java new file mode 100644 index 0000000..9521004 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackend.java
@@ -0,0 +1,98 @@ +// 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.auth; + +import com.google.gerrit.entities.AccountGroup; +import com.google.gerrit.entities.GroupDescription; +import com.google.gerrit.entities.GroupReference; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.AbstractGroupBackend; +import com.google.gerrit.server.account.GroupMembership; +import com.google.gerrit.server.account.ListGroupMembership; +import com.google.gerrit.server.group.SystemGroupBackend; +import com.google.gerrit.server.project.ProjectState; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.util.Arrays; +import java.util.Collection; + +/** Backend to expose the pull-replication internal user group membership. */ +@Singleton +class PullReplicationGroupBackend extends AbstractGroupBackend { + public static final AccountGroup.UUID INTERNAL_GROUP_UUID = + AccountGroup.uuid("pullreplication:internal-user"); + public static final String INTERNAL_GROUP_NAME = "Pull-replication Internal User"; + public static final String NAME_PREFIX = "pullreplication/"; + public static final GroupDescription.Basic INTERNAL_GROUP_DESCRIPTION = + new GroupDescription.Basic() { + + @Override + public String getUrl() { + return null; + } + + @Override + public String getName() { + return INTERNAL_GROUP_NAME; + } + + @Override + public AccountGroup.UUID getGroupUUID() { + return INTERNAL_GROUP_UUID; + } + + @Override + public String getEmailAddress() { + return null; + } + }; + private final PullReplicationInternalUser internalUser; + + static final ListGroupMembership INTERNAL_GROUP_MEMBERSHIP = + new ListGroupMembership( + Arrays.asList(INTERNAL_GROUP_UUID, SystemGroupBackend.ANONYMOUS_USERS)); + + @Inject + public PullReplicationGroupBackend(PullReplicationInternalUser internalUser) { + this.internalUser = internalUser; + } + + @Override + public boolean handles(AccountGroup.UUID uuid) { + return INTERNAL_GROUP_UUID.equals(uuid); + } + + @Override + public GroupDescription.Basic get(AccountGroup.UUID uuid) { + return handles(uuid) ? INTERNAL_GROUP_DESCRIPTION : null; + } + + @Override + public Collection<GroupReference> suggest(String name, ProjectState project) { + return Arrays.asList( + NAME_PREFIX.contains(name.toLowerCase()) + ? GroupReference.create(INTERNAL_GROUP_UUID, INTERNAL_GROUP_NAME) + : GroupReference.create(name)); + } + + @Override + public GroupMembership membershipsOf(CurrentUser user) { + if (user.equals(internalUser)) { + return INTERNAL_GROUP_MEMBERSHIP; + } + + return ListGroupMembership.EMPTY; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupModule.java new file mode 100644 index 0000000..583ba8e --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupModule.java
@@ -0,0 +1,26 @@ +// 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.auth; + +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.server.account.GroupBackend; +import com.google.inject.AbstractModule; + +public class PullReplicationGroupModule extends AbstractModule { + @Override + protected void configure() { + DynamicSet.bind(binder(), GroupBackend.class).to(PullReplicationGroupBackend.class); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationInternalUser.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationInternalUser.java new file mode 100644 index 0000000..e8b7666 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationInternalUser.java
@@ -0,0 +1,35 @@ +// 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.auth; + +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.server.PluginUser; +import com.google.gerrit.server.account.GroupMembership; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +@Singleton +public class PullReplicationInternalUser extends PluginUser { + + @Inject + protected PullReplicationInternalUser(@PluginName String pluginName) { + super(pluginName); + } + + @Override + public GroupMembership getEffectiveGroups() { + return PullReplicationGroupBackend.INTERNAL_GROUP_MEMBERSHIP; + } +}
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 ed919de..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; @@ -26,12 +25,14 @@ import com.google.gerrit.entities.Project.NameKey; import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.restapi.Url; +import com.google.gerrit.server.config.GerritInstanceId; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.inject.Inject; 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; @@ -42,19 +43,19 @@ 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.AuthScope; +import org.apache.http.auth.AuthenticationException; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.CredentialsProvider; import org.apache.http.client.ResponseHandler; 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.protocol.HttpClientContext; +import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.auth.BasicScheme; import org.apache.http.message.BasicHeader; import org.apache.http.util.EntityUtils; import org.eclipse.jgit.transport.CredentialItem; @@ -70,9 +71,11 @@ private final CredentialsFactory credentials; private final SourceHttpClient.Factory httpClientFactory; private final Source source; - private final String instanceLabel; + private final String instanceId; private final String pluginName; private final SyncRefsFilter syncRefsFilter; + private final BearerTokenProvider bearerTokenProvider; + private final String urlAuthenticationPrefix; @Inject FetchRestApiClient( @@ -81,18 +84,26 @@ ReplicationConfig replicationConfig, SyncRefsFilter syncRefsFilter, @PluginName String pluginName, + @Nullable @GerritInstanceId String instanceId, + BearerTokenProvider bearerTokenProvider, @Assisted Source source) { this.credentials = credentials; this.httpClientFactory = httpClientFactory; this.source = source; this.pluginName = pluginName; this.syncRefsFilter = syncRefsFilter; - this.instanceLabel = - Strings.nullToEmpty( + this.instanceId = + Optional.ofNullable( replicationConfig.getConfig().getString("replication", null, "instanceLabel")) + .orElse(instanceId) .trim(); + requireNonNull( - Strings.emptyToNull(instanceLabel), "replication.instanceLabel cannot be null or empty"); + 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) @@ -101,24 +112,21 @@ @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( new StringEntity( String.format( "{\"label\":\"%s\", \"ref_name\": \"%s\", \"async\":%s}", - instanceLabel, refName, callAsync), + instanceId, refName, callAsync), StandardCharsets.UTF_8)); post.addHeader(new BasicHeader("Content-Type", "application/json")); post.addHeader( PullReplicationApiRequestMetrics.HTTP_HEADER_X_START_TIME_NANOS, Long.toString(startTimeNanos)); - return httpClientFactory.create(source).execute(post, this, getContext(targetUri)); + return executeRequest(post, bearerTokenProvider.get(), targetUri); } /* (non-Javadoc) @@ -126,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(put, this, getContext(uri)); + return executeRequest(put, bearerTokenProvider.get(), uri); } /* (non-Javadoc) @@ -140,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(delete, this, getContext(apiUri)); + return executeRequest(delete, bearerTokenProvider.get(), apiUri); } /* (non-Javadoc) @@ -153,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(req, this, getContext(apiUri)); + return executeRequest(req, bearerTokenProvider.get(), apiUri); } /* (non-Javadoc) @@ -180,14 +184,14 @@ } else { requireNull(revisionData, "DELETE ref-updates cannot be associated with a RevisionData"); } - RevisionInput input = new RevisionInput(instanceLabel, refName, revisionData); + 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(post, this, getContext(targetUri)); + return executeRequest(post, bearerTokenProvider.get(), targetUri); } @Override @@ -199,22 +203,25 @@ } RevisionData[] inputData = new RevisionData[revisionData.size()]; - RevisionsInput input = - new RevisionsInput(instanceLabel, refName, revisionData.toArray(inputData)); + 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(post, this, getContext(targetUri)); + 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) { @@ -242,30 +249,37 @@ return new HttpResult(response.getStatusLine().getStatusCode(), responseBody); } - private HttpClientContext getContext(URIish targetUri) { - HttpClientContext ctx = HttpClientContext.create(); - ctx.setCredentialsProvider(adapt(credentials.create(source.getRemoteConfigName()), targetUri)); - return ctx; + 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 CredentialsProvider adapt(org.eclipse.jgit.transport.CredentialsProvider cp, URIish uri) { + private HttpRequestBase withBasicAuthentication(URIish targetUri, HttpRequestBase req) { + org.eclipse.jgit.transport.CredentialsProvider cp = + credentials.create(source.getRemoteConfigName()); CredentialItem.Username user = new CredentialItem.Username(); CredentialItem.Password pass = new CredentialItem.Password(); - if (cp.supports(user, pass) && cp.get(uri, user, pass)) { - CredentialsProvider adapted = new BasicCredentialsProvider(); - adapted.setCredentials( - AuthScope.ANY, - new UsernamePasswordCredentials(user.getValue(), new String(pass.getValue()))); - return adapted; + if (cp.supports(user, pass) && cp.get(targetUri, user, pass)) { + UsernamePasswordCredentials creds = + new UsernamePasswordCredentials(user.getValue(), new String(pass.getValue())); + try { + req.addHeader(new BasicScheme().authenticate(creds, req, null)); + } catch (AuthenticationException e) { + logger.atFine().log(String.format("Anonymous Basic Authentication for uri: %s", targetUri)); + } } - return null; + 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/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpClient.java index 7bfc7d1..6254a42 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpClient.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/HttpClient.java
@@ -18,14 +18,11 @@ import org.apache.http.client.ClientProtocolException; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.protocol.HttpContext; /** HTTP client for executing URI requests to a remote site */ public interface HttpClient { public <T> T execute( - final HttpUriRequest request, - final ResponseHandler<? extends T> responseHandler, - final HttpContext context) + final HttpUriRequest request, final ResponseHandler<? extends T> responseHandler) throws ClientProtocolException, IOException; }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/SourceHttpClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/SourceHttpClient.java index ee0fe79..fa700e3 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/SourceHttpClient.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/SourceHttpClient.java
@@ -25,7 +25,6 @@ import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.apache.http.protocol.HttpContext; /** Apache HTTP client implementation based on Source-specific parameters */ public class SourceHttpClient implements HttpClient { @@ -41,8 +40,7 @@ } @Override - public <T> T execute( - HttpUriRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context) + public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler) throws ClientProtocolException, IOException { return source .memoize( @@ -51,7 +49,7 @@ .setConnectionManager(customConnectionManager(source)) .setDefaultRequestConfig(customRequestConfig(source)) .build()) - .execute(request, responseHandler, context); + .execute(request, responseHandler); } private static RequestConfig customRequestConfig(Source source) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java new file mode 100644 index 0000000..0f092e5 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListener.java
@@ -0,0 +1,120 @@ +// 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.event; + +import static java.util.Objects.requireNonNull; + +import com.google.common.base.Strings; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.entities.Project.NameKey; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.server.config.GerritInstanceId; +import com.google.gerrit.server.events.Event; +import com.google.gerrit.server.events.EventListener; +import com.google.gerrit.server.events.ProjectCreatedEvent; +import com.google.gerrit.server.events.RefUpdatedEvent; +import com.google.gerrit.server.git.WorkQueue; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.googlesource.gerrit.plugins.replication.pull.FetchOne; +import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction; +import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob; +import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob.Factory; +import com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction; +import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics; +import org.eclipse.jgit.lib.ObjectId; + +public class StreamEventListener implements EventListener { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private String instanceId; + private WorkQueue workQueue; + private ProjectInitializationAction projectInitializationAction; + + private Factory fetchJobFactory; + private final Provider<PullReplicationApiRequestMetrics> metricsProvider; + + @Inject + public StreamEventListener( + @Nullable @GerritInstanceId String instanceId, + ProjectInitializationAction projectInitializationAction, + WorkQueue workQueue, + FetchJob.Factory fetchJobFactory, + Provider<PullReplicationApiRequestMetrics> metricsProvider) { + this.instanceId = instanceId; + this.projectInitializationAction = projectInitializationAction; + this.workQueue = workQueue; + this.fetchJobFactory = fetchJobFactory; + this.metricsProvider = metricsProvider; + + requireNonNull( + Strings.emptyToNull(this.instanceId), "gerrit.instanceId cannot be null or empty"); + } + + @Override + public void onEvent(Event event) { + if (!instanceId.equals(event.instanceId)) { + PullReplicationApiRequestMetrics metrics = metricsProvider.get(); + metrics.start(event); + if (event instanceof RefUpdatedEvent) { + RefUpdatedEvent refUpdatedEvent = (RefUpdatedEvent) event; + if (!isProjectDelete(refUpdatedEvent)) { + fetchRefsAsync( + refUpdatedEvent.getRefName(), + refUpdatedEvent.instanceId, + refUpdatedEvent.getProjectNameKey(), + metrics); + } + } + if (event instanceof ProjectCreatedEvent) { + ProjectCreatedEvent projectCreatedEvent = (ProjectCreatedEvent) event; + try { + projectInitializationAction.initProject(getProjectRepositoryName(projectCreatedEvent)); + fetchRefsAsync( + FetchOne.ALL_REFS, + projectCreatedEvent.instanceId, + projectCreatedEvent.getProjectNameKey(), + metrics); + } catch (AuthException | PermissionBackendException e) { + logger.atSevere().withCause(e).log( + "Cannot initialise project:%s", projectCreatedEvent.projectName); + } + } + } + } + + private boolean isProjectDelete(RefUpdatedEvent event) { + return RefNames.isConfigRef(event.getRefName()) + && ObjectId.zeroId().equals(ObjectId.fromString(event.refUpdate.get().newRev)); + } + + protected void fetchRefsAsync( + String refName, + String sourceInstanceId, + NameKey projectNameKey, + PullReplicationApiRequestMetrics metrics) { + FetchAction.Input input = new FetchAction.Input(); + input.refName = refName; + input.label = sourceInstanceId; + workQueue.getDefaultQueue().submit(fetchJobFactory.create(projectNameKey, input, metrics)); + } + + private String getProjectRepositoryName(ProjectCreatedEvent projectCreatedEvent) { + return String.format("%s.git", projectCreatedEvent.projectName); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventModule.java new file mode 100644 index 0000000..2389678 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventModule.java
@@ -0,0 +1,27 @@ +// 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.event; + +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.server.events.EventListener; +import com.google.inject.AbstractModule; + +public class StreamEventModule extends AbstractModule { + + @Override + protected void configure() { + DynamicSet.bind(binder(), EventListener.class).to(StreamEventListener.class); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java index 2bb1caf..36356e9 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java
@@ -15,8 +15,9 @@ package com.googlesource.gerrit.plugins.replication.pull.fetch; import com.google.gerrit.entities.Project; -import com.google.gerrit.server.git.LocalDiskRepositoryManager; +import com.google.gerrit.server.git.GitRepositoryManager; import com.google.inject.Inject; +import com.googlesource.gerrit.plugins.replication.pull.LocalGitRepositoryManagerProvider; import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData; import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData; import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException; @@ -30,15 +31,15 @@ public class ApplyObject { - private final LocalDiskRepositoryManager gitManager; + private final GitRepositoryManager gitManager; - // NOTE: We do need specifically the LocalDiskRepositoryManager to make sure + // NOTE: We do need specifically the local GitRepositoryManager to make sure // to be able to write onto the directly physical repository without any wrapper. // Using for instance the multi-site wrapper injected by Guice would result // in a split-brain because of the misalignment of local vs. global refs values. @Inject - public ApplyObject(LocalDiskRepositoryManager gitManager) { - this.gitManager = gitManager; + public ApplyObject(LocalGitRepositoryManagerProvider gitManagerProvider) { + this.gitManager = gitManagerProvider.get(); } public RefUpdateState apply(Project.NameKey name, RefSpec refSpec, RevisionData[] revisionsData)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java index 5de82f1..89972cf 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java
@@ -18,34 +18,22 @@ import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.googlesource.gerrit.plugins.replication.CredentialsFactory; -import com.googlesource.gerrit.plugins.replication.pull.SourceConfiguration; import java.io.IOException; import java.util.List; import java.util.stream.Collectors; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.CredentialsProvider; -import org.eclipse.jgit.transport.FetchResult; -import org.eclipse.jgit.transport.RefSpec; -import org.eclipse.jgit.transport.RemoteConfig; -import org.eclipse.jgit.transport.Transport; -import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.*; public class JGitFetch implements Fetch { - private final RemoteConfig config; - private final CredentialsProvider credentialsProvider; URIish uri; Repository git; + private final TransportProvider transportProvider; @Inject public JGitFetch( - SourceConfiguration sourceConfig, - CredentialsFactory cpFactory, - @Assisted URIish uri, - @Assisted Repository git) { - this.config = sourceConfig.getRemoteConfig(); - this.credentialsProvider = cpFactory.create(config.getName()); + TransportProvider transportProvider, @Assisted URIish uri, @Assisted Repository git) { + this.transportProvider = transportProvider; this.uri = uri; this.git = git; } @@ -53,7 +41,7 @@ @Override public List<RefUpdateState> fetch(List<RefSpec> refs) throws IOException { FetchResult res; - try (Transport tn = Transport.open(git, uri)) { + try (Transport tn = transportProvider.open(git, uri)) { res = fetchVia(tn, refs); } return res.getTrackingRefUpdates().stream() @@ -62,9 +50,6 @@ } private FetchResult fetchVia(Transport tn, List<RefSpec> fetchRefSpecs) throws IOException { - tn.applyConfig(config); - tn.setCredentialsProvider(credentialsProvider); - repLog.info("Fetch references {} from {}", fetchRefSpecs, uri); return tn.fetch(NullProgressMonitor.INSTANCE, fetchRefSpecs); }
diff --git a/src/main/java/org/eclipse/jgit/transport/TransportHttpWithBearerToken.java b/src/main/java/org/eclipse/jgit/transport/TransportHttpWithBearerToken.java new file mode 100644 index 0000000..68ff6f8 --- /dev/null +++ b/src/main/java/org/eclipse/jgit/transport/TransportHttpWithBearerToken.java
@@ -0,0 +1,74 @@ +// 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 org.eclipse.jgit.transport; + +import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URL; +import java.util.Collection; +import java.util.Set; +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.http.HttpConnection; + +/** + * This is a hack in order to allow git over http/https with Bearer Token Authentication. + * + * <p>Currently {@link org.eclipse.jgit.transport.TransportHttp} does NOT provide Bearer Token + * Authentication and unfortunately it is not possible to extend the functionality because some + * classes, methods and instance variables are private or protected. This package in the pull + * replication plugin provides the visibility needed and allows to extend the functionality. + * + * <p>It is important to mention that in the case of git push operation, this class cannot be used + * because the push operation needs to initialise a push hook: {@link + * org.eclipse.jgit.transport.Transport#push(ProgressMonitor, Collection, OutputStream)} and it is + * defined as a private in the code. + */ +public class TransportHttpWithBearerToken extends TransportHttp { + + private static final String SCHEME_HTTP = "http"; + private static final String SCHEME_HTTPS = "https"; + private static final Set<String> SCHEMES_ALLOWED = ImmutableSet.of(SCHEME_HTTP, SCHEME_HTTPS); + private final String bearerToken; + + public TransportHttpWithBearerToken(Repository local, URIish uri, String bearerToken) + throws NotSupportedException { + super(local, uri); + this.bearerToken = bearerToken; + } + + protected HttpConnection httpOpen(String method, URL u, AcceptEncoding acceptEncoding) + throws IOException { + HttpConnection conn = super.httpOpen(method, u, acceptEncoding); + conn.setRequestProperty(HDR_AUTHORIZATION, "Bearer " + bearerToken); // $NON-NLS-1$ + return conn; + } + + /** + * This method copies the behaviour of {@link + * org.eclipse.jgit.transport.TransportProtocol#canHandle(URIish, Repository, String)} in the case + * of {@link org.eclipse.jgit.transport.TransportHttp} where scheme, host and path are compulsory. + */ + public static boolean canHandle(URIish uri) { + return SCHEMES_ALLOWED.contains(uri.getScheme()) + && !Strings.isNullOrEmpty(uri.getHost()) + && !Strings.isNullOrEmpty(uri.getPath()); + } +}
diff --git a/src/main/java/org/eclipse/jgit/transport/TransportProvider.java b/src/main/java/org/eclipse/jgit/transport/TransportProvider.java new file mode 100644 index 0000000..ca15b6c --- /dev/null +++ b/src/main/java/org/eclipse/jgit/transport/TransportProvider.java
@@ -0,0 +1,68 @@ +// 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 org.eclipse.jgit.transport; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.googlesource.gerrit.plugins.replication.CredentialsFactory; +import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider; +import com.googlesource.gerrit.plugins.replication.pull.SourceConfiguration; +import java.util.Optional; +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.Repository; + +/** + * This class is responsible for providing a Custom Git HTTP Transport with Bearer Token + * Authentication or a concrete implementation of {@link org.eclipse.jgit.transport.Transport}. + */ +@Singleton +public class TransportProvider { + private final RemoteConfig remoteConfig; + private final CredentialsProvider credentialsProvider; + private final Optional<String> bearerToken; + + @Inject + public TransportProvider( + SourceConfiguration sourceConfig, + CredentialsFactory cpFactory, + BearerTokenProvider bearerTokenProvider) { + this.remoteConfig = sourceConfig.getRemoteConfig(); + this.credentialsProvider = cpFactory.create(remoteConfig.getName()); + this.bearerToken = bearerTokenProvider.get(); + } + + public Transport open(Repository local, URIish uri) + throws NotSupportedException, TransportException { + return (bearerToken.isPresent() && TransportHttpWithBearerToken.canHandle(uri)) + ? provideTransportHttpWithBearerToken(local, uri) + : provideNativeTransport(local, uri); + } + + private Transport provideTransportHttpWithBearerToken(Repository local, URIish uri) + throws NotSupportedException { + Transport tn = new TransportHttpWithBearerToken(local, uri, bearerToken.get()); + tn.applyConfig(remoteConfig); + return tn; + } + + private Transport provideNativeTransport(Repository local, URIish uri) + throws NotSupportedException, TransportException { + Transport tn = Transport.open(local, uri); + tn.applyConfig(remoteConfig); + tn.setCredentialsProvider(credentialsProvider); + return tn; + } +}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index b2470a4..d3a2859 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -31,8 +31,13 @@ threads = 3 authGroup = Public Mirror Group authGroup = Second Public Mirror Group - [replication] - instanceLabel = host-one +``` + +And make sure that instanceId is setup in `$site_path/etc/gerrit.config`: + +``` +[gerrit] + instanceId = host-one ``` Then reload the replication plugin to pick up the new configuration: @@ -121,6 +126,21 @@ provided in the remote configuration section which name is equal to instanceLabel. + Deprecated: This property is kept for backward compatibility and + will be removed in the future release. Use [gerrit.instanceId](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#gerrit.instanceId) + instead. + +replication.consumeStreamEvents +: Use stream events to trigger pull-replication actions alongside the + REST approach. This mechanism is useful together with event-broker + and multi-site to provide backfill mechanism when a node has to + catch up with the events after being unreachable. + + NOTE: When `consumeStreamEvents` is enabled gerrit.instanceId + instead of [replication.instanceLabel](https://gerrit.googlesource.com/plugins/pull-replication/+/refs/heads/stable-3.4/src/main/resources/Documentation/config.md#replication.instanceLabel) must be used. + + Default: false + replication.maxConnectionsPerRoute : Maximum number of HTTP connections per one HTTP route. @@ -441,7 +461,6 @@ autoReload = true replicateOnStartup = false [replication] - instanceLabel = host-one lockErrorMaxRetries = 5 maxRetries = 5 ``` @@ -477,7 +496,6 @@ autoReload = true replicateOnStartup = false [replication] - instanceLabel = host-one lockErrorMaxRetries = 5 maxRetries = 5 @@ -500,8 +518,15 @@ The optional file `$site_path/secure.config` is a Git-style config file that provides secure values that should not be world-readable, -such as passwords. Passwords for HTTP remotes can be obtained from -this file. +such as passwords. The HTTP authentication can be configured in 2 +different flavours: + +*HTTP Bearer Token Authentication* + +auth.bearerToken +: shared secret configured on all replication endpoints. + +*HTTP Basic Authentication* remote.NAME.username : Username to use for HTTP authentication on this remote, if not
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java index cb5af42..ee5876f 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationFanoutConfigIT.java
@@ -23,6 +23,7 @@ import com.google.gerrit.acceptance.SkipProjectClone; import com.google.gerrit.acceptance.TestPlugin; import com.google.gerrit.acceptance.UseLocalDisk; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; import com.google.gerrit.entities.Project; import com.google.gerrit.extensions.api.projects.BranchInput; @@ -95,6 +96,7 @@ } @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) public void shouldReplicateNewChangeRef() throws Exception { testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX)); @@ -122,6 +124,7 @@ } @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) public void shouldReplicateNewChangeRefAfterConfigReloaded() throws Exception { testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX)); @@ -157,6 +160,7 @@ } @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) public void shouldReplicateNewBranch() throws Exception { String testProjectName = project + TEST_REPLICATION_SUFFIX; createTestProject(testProjectName); @@ -190,6 +194,7 @@ } @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) public void shouldAutoReloadConfiguration() throws Exception { SourcesCollection sources = getInstance(SourcesCollection.class); AutoReloadConfigDecorator autoReloadConfigDecorator = @@ -202,6 +207,7 @@ } @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) public void shouldAutoReloadConfigurationWhenRemoteConfigAdded() throws Exception { FileBasedConfig newRemoteConfig = new FileBasedConfig( @@ -224,6 +230,7 @@ } @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) public void shouldAutoReloadConfigurationWhenRemoteConfigDeleted() throws Exception { SourcesCollection sources = getInstance(SourcesCollection.class); AutoReloadConfigDecorator autoReloadConfigDecorator = @@ -259,7 +266,6 @@ } private void setReplicationSource(String remoteName) throws IOException { - config.setString("replication", null, "instanceLabel", remoteName); config.setBoolean("gerrit", null, "autoReload", true); config.save(); }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java index ec60793..f3c62c5 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
@@ -19,60 +19,37 @@ import static com.google.gerrit.acceptance.GitUtil.pushOne; import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; -import static java.util.stream.Collectors.toList; -import com.google.common.flogger.FluentLogger; -import com.google.gerrit.acceptance.LightweightPluginDaemonTest; import com.google.gerrit.acceptance.PushOneCommit.Result; import com.google.gerrit.acceptance.SkipProjectClone; import com.google.gerrit.acceptance.TestPlugin; import com.google.gerrit.acceptance.UseLocalDisk; -import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.acceptance.config.GerritConfig; import com.google.gerrit.entities.Permission; -import com.google.gerrit.entities.Project; import com.google.gerrit.entities.Project.NameKey; import com.google.gerrit.entities.RefNames; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.projects.BranchInput; -import com.google.gerrit.extensions.common.Input; import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; import com.google.gerrit.extensions.events.HeadUpdatedListener; import com.google.gerrit.extensions.events.ProjectDeletedListener; -import com.google.gerrit.extensions.registration.DynamicSet; -import com.google.gerrit.extensions.restapi.AuthException; -import com.google.gerrit.extensions.restapi.BadRequestException; -import com.google.gerrit.extensions.restapi.ResourceConflictException; -import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.extensions.restapi.RestModifyView; -import com.google.gerrit.server.config.SitePaths; -import com.google.gerrit.server.project.ProjectResource; -import com.google.inject.Inject; -import com.google.inject.Singleton; import com.googlesource.gerrit.plugins.replication.AutoReloadConfigDecorator; -import java.io.File; import java.io.IOException; -import java.nio.file.Path; -import java.time.Duration; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.function.Supplier; -import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.transport.RemoteRefUpdate; import org.eclipse.jgit.transport.RemoteRefUpdate.Status; -import org.eclipse.jgit.util.FS; import org.junit.Test; @SkipProjectClone @@ -81,49 +58,31 @@ name = "pull-replication", sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule", httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule") -public class PullReplicationIT extends LightweightPluginDaemonTest { - private static final Optional<String> ALL_PROJECTS = Optional.empty(); - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - private static final int TEST_REPLICATION_DELAY = 1; - private static final Duration TEST_TIMEOUT = Duration.ofSeconds(TEST_REPLICATION_DELAY * 2000); - private static final String TEST_REPLICATION_SUFFIX = "suffix1"; - private static final String TEST_REPLICATION_REMOTE = "remote1"; +public class PullReplicationIT extends PullReplicationSetupBase { - @Inject private SitePaths sitePaths; - @Inject private ProjectOperations projectOperations; - @Inject private DynamicSet<ProjectDeletedListener> deletedListeners; - private Path gitPath; - private FileBasedConfig config; - private FileBasedConfig secureConfig; + @Override + protected void setReplicationSource( + String remoteName, List<String> replicaSuffixes, Optional<String> project) + throws IOException { + List<String> fetchUrls = + buildReplicaURLs(replicaSuffixes, s -> gitPath.resolve("${name}" + s + ".git").toString()); + config.setStringList("remote", remoteName, "url", fetchUrls); + config.setString("remote", remoteName, "apiUrl", adminRestSession.url()); + config.setString("remote", remoteName, "fetch", "+refs/*:refs/*"); + config.setInt("remote", remoteName, "timeout", 600); + config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY); + project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj)); + config.setBoolean("gerrit", null, "autoReload", true); + config.save(); + } @Override public void setUpTestPlugin() throws Exception { setUpTestPlugin(false); } - protected void setUpTestPlugin(boolean loadExisting) throws Exception { - gitPath = sitePaths.site_path.resolve("git"); - - File configFile = sitePaths.etc_dir.resolve("replication.config").toFile(); - config = new FileBasedConfig(configFile, FS.DETECTED); - if (loadExisting && configFile.exists()) { - config.load(); - } - setReplicationSource( - TEST_REPLICATION_REMOTE, - TEST_REPLICATION_SUFFIX, - ALL_PROJECTS); // Simulates a full replication.config initialization - config.save(); - - secureConfig = - new FileBasedConfig(sitePaths.etc_dir.resolve("secure.config").toFile(), FS.DETECTED); - setReplicationCredentials(TEST_REPLICATION_REMOTE, admin.username(), admin.httpPassword()); - secureConfig.save(); - - super.setUpTestPlugin(); - } - @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) public void shouldReplicateNewChangeRef() throws Exception { testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX)); @@ -151,6 +110,7 @@ } @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) public void shouldReplicateNewBranch() throws Exception { String testProjectName = project + TEST_REPLICATION_SUFFIX; createTestProject(testProjectName); @@ -185,6 +145,7 @@ @Test @UseLocalDisk + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) public void shouldReplicateForceUpdatedBranch() throws Exception { boolean forcedPush = true; String testProjectName = project + TEST_REPLICATION_SUFFIX; @@ -254,6 +215,7 @@ } @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) public void shouldReplicateNewChangeRefCGitClient() throws Exception { AutoReloadConfigDecorator autoReloadConfigDecorator = getInstance(AutoReloadConfigDecorator.class); @@ -289,6 +251,7 @@ } @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) public void shouldReplicateNewBranchCGitClient() throws Exception { AutoReloadConfigDecorator autoReloadConfigDecorator = getInstance(AutoReloadConfigDecorator.class); @@ -330,6 +293,7 @@ } @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) public void shouldReplicateProjectDeletion() throws Exception { String projectToDelete = project.get(); setReplicationSource(TEST_REPLICATION_REMOTE, "", Optional.of(projectToDelete)); @@ -358,6 +322,7 @@ } @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) public void shouldReplicateHeadUpdate() throws Exception { String testProjectName = project.get(); setReplicationSource(TEST_REPLICATION_REMOTE, "", Optional.of(testProjectName)); @@ -389,80 +354,32 @@ }); } - private Ref getRef(Repository repo, String branchName) throws IOException { - return repo.getRefDatabase().exactRef(branchName); - } + @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) + @GerritConfig(name = "container.replica", value = "true") + public void shouldReplicateNewChangeRefToReplica() throws Exception { + testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX)); - private Ref checkedGetRef(Repository repo, String branchName) { - try { - return repo.getRefDatabase().exactRef(branchName); - } catch (Exception e) { - logger.atSevere().withCause(e).log("failed to get ref %s in repo %s", branchName, repo); - return null; - } - } + Result pushResult = createChange(); + RevCommit sourceCommit = pushResult.getCommit(); + String sourceRef = pushResult.getPatchSet().refName(); - private void setReplicationSource( - String remoteName, String replicaSuffix, Optional<String> project) - throws IOException, ConfigInvalidException { - setReplicationSource(remoteName, Arrays.asList(replicaSuffix), project); - } + ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class); + GitReferenceUpdatedListener.Event event = + new FakeGitReferenceUpdatedEvent( + project, + sourceRef, + ObjectId.zeroId().getName(), + sourceCommit.getId().getName(), + ReceiveCommand.Type.CREATE); + pullReplicationQueue.onGitReferenceUpdated(event); - private void setReplicationSource( - String remoteName, List<String> replicaSuffixes, Optional<String> project) - throws IOException, ConfigInvalidException { + try (Repository repo = repoManager.openRepository(project)) { + waitUntil(() -> checkedGetRef(repo, sourceRef) != null); - List<String> replicaUrls = - replicaSuffixes.stream() - .map(suffix -> gitPath.resolve("${name}" + suffix + ".git").toString()) - .collect(toList()); - config.setString("replication", null, "instanceLabel", remoteName); - config.setStringList("remote", remoteName, "url", replicaUrls); - config.setString("remote", remoteName, "apiUrl", adminRestSession.url()); - config.setString("remote", remoteName, "fetch", "+refs/*:refs/*"); - config.setInt("remote", remoteName, "timeout", 600); - config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY); - project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj)); - config.setBoolean("gerrit", null, "autoReload", true); - config.save(); - } - - private void setReplicationCredentials(String remoteName, String username, String password) - throws IOException { - secureConfig.setString("remote", remoteName, "username", username); - secureConfig.setString("remote", remoteName, "password", password); - secureConfig.save(); - } - - private void waitUntil(Supplier<Boolean> waitCondition) throws InterruptedException { - WaitUtil.waitUntil(waitCondition, TEST_TIMEOUT); - } - - private <T> T getInstance(Class<T> classObj) { - return plugin.getSysInjector().getInstance(classObj); - } - - private Project.NameKey createTestProject(String name) throws Exception { - return projectOperations.newProject().name(name).create(); - } - - @Singleton - public static class FakeDeleteProjectPlugin implements RestModifyView<ProjectResource, Input> { - private int deleteEndpointCalls; - - FakeDeleteProjectPlugin() { - this.deleteEndpointCalls = 0; - } - - @Override - public Response<?> apply(ProjectResource resource, Input input) - throws AuthException, BadRequestException, ResourceConflictException, Exception { - deleteEndpointCalls += 1; - return Response.ok(); - } - - int getDeleteEndpointCalls() { - return deleteEndpointCalls; + Ref targetBranchRef = getRef(repo, sourceRef); + assertThat(targetBranchRef).isNotNull(); + assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId()); } } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationSetupBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationSetupBase.java new file mode 100644 index 0000000..e07d481 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationSetupBase.java
@@ -0,0 +1,124 @@ +// 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.replication.pull; + +import static java.util.stream.Collectors.toList; + +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.acceptance.LightweightPluginDaemonTest; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.entities.Project.NameKey; +import com.google.gerrit.extensions.events.ProjectDeletedListener; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.server.config.SitePaths; +import com.google.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; + +public abstract class PullReplicationSetupBase extends LightweightPluginDaemonTest { + + protected static final Optional<String> ALL_PROJECTS = Optional.empty(); + protected static final FluentLogger logger = FluentLogger.forEnclosingClass(); + protected static final int TEST_REPLICATION_DELAY = 1; + protected static final Duration TEST_TIMEOUT = Duration.ofSeconds(TEST_REPLICATION_DELAY * 2000); + protected static final String TEST_REPLICATION_SUFFIX = "suffix1"; + protected static final String TEST_REPLICATION_REMOTE = "remote1"; + @Inject protected SitePaths sitePaths; + @Inject protected ProjectOperations projectOperations; + @Inject protected DynamicSet<ProjectDeletedListener> deletedListeners; + protected Path gitPath; + protected FileBasedConfig config; + protected FileBasedConfig secureConfig; + + protected void setUpTestPlugin(boolean loadExisting) throws Exception { + gitPath = sitePaths.site_path.resolve("git"); + + File configFile = sitePaths.etc_dir.resolve("replication.config").toFile(); + config = new FileBasedConfig(configFile, FS.DETECTED); + if (loadExisting && configFile.exists()) { + config.load(); + } + setReplicationSource( + TEST_REPLICATION_REMOTE, + TEST_REPLICATION_SUFFIX, + ALL_PROJECTS); // Simulates a full replication.config initialization + config.save(); + + secureConfig = + new FileBasedConfig(sitePaths.etc_dir.resolve("secure.config").toFile(), FS.DETECTED); + setReplicationCredentials(TEST_REPLICATION_REMOTE, admin.username(), admin.httpPassword()); + secureConfig.save(); + + super.setUpTestPlugin(); + } + + protected Ref getRef(Repository repo, String branchName) throws IOException { + return repo.getRefDatabase().exactRef(branchName); + } + + protected Ref checkedGetRef(Repository repo, String branchName) { + try { + return repo.getRefDatabase().exactRef(branchName); + } catch (Exception e) { + logger.atSevere().withCause(e).log("failed to get ref %s in repo %s", branchName, repo); + return null; + } + } + + protected void setReplicationSource( + String remoteName, String replicaSuffix, Optional<String> project) + throws IOException, ConfigInvalidException { + setReplicationSource(remoteName, Arrays.asList(replicaSuffix), project); + } + + protected abstract void setReplicationSource( + String remoteName, List<String> replicaSuffixes, Optional<String> project) throws IOException; + + protected void setReplicationCredentials(String remoteName, String username, String password) + throws IOException { + secureConfig.setString("remote", remoteName, "username", username); + secureConfig.setString("remote", remoteName, "password", password); + secureConfig.save(); + } + + protected void waitUntil(Supplier<Boolean> waitCondition) throws InterruptedException { + WaitUtil.waitUntil(waitCondition, TEST_TIMEOUT); + } + + protected <T> T getInstance(Class<T> classObj) { + return plugin.getSysInjector().getInstance(classObj); + } + + protected NameKey createTestProject(String name) throws Exception { + return projectOperations.newProject().name(name).create(); + } + + protected List<String> buildReplicaURLs( + List<String> replicaSuffixes, Function<String, String> toURL) { + return replicaSuffixes.stream().map(suffix -> toURL.apply(suffix)).collect(toList()); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationWithGitHttpTransportProtocolIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationWithGitHttpTransportProtocolIT.java new file mode 100644 index 0000000..d8e5947 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationWithGitHttpTransportProtocolIT.java
@@ -0,0 +1,102 @@ +// 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.replication.pull; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.acceptance.PushOneCommit.Result; +import com.google.gerrit.acceptance.SkipProjectClone; +import com.google.gerrit.acceptance.TestPlugin; +import com.google.gerrit.acceptance.UseLocalDisk; +import com.google.gerrit.acceptance.config.GerritConfig; +import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.junit.Test; + +@SkipProjectClone +@UseLocalDisk +@TestPlugin( + name = "pull-replication", + sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule", + httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule") +public class PullReplicationWithGitHttpTransportProtocolIT extends PullReplicationSetupBase { + + @Override + protected void setReplicationSource( + String remoteName, List<String> replicaSuffixes, Optional<String> project) + throws IOException { + List<String> fetchUrls = + buildReplicaURLs(replicaSuffixes, s -> adminRestSession.url() + "/${name}" + s + ".git"); + config.setStringList("remote", remoteName, "url", fetchUrls); + config.setString("remote", remoteName, "apiUrl", adminRestSession.url()); + config.setString("remote", remoteName, "fetch", "+refs/*:refs/*"); + config.setInt("remote", remoteName, "timeout", 600); + config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY); + project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj)); + config.setBoolean("gerrit", null, "autoReload", true); + config.save(); + } + + @Override + public void setUpTestPlugin() throws Exception { + setUpTestPlugin(false); + } + + @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) + public void shouldReplicateWithBasicAuthentication() throws Exception { + runTest(); + } + + @Test + @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE) + @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token") + public void shouldReplicateWithBearerTokenAuthentication() throws Exception { + runTest(); + } + + private void runTest() throws Exception { + testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX)); + + Result pushResult = createChange(); + RevCommit sourceCommit = pushResult.getCommit(); + String sourceRef = pushResult.getPatchSet().refName(); + + ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class); + GitReferenceUpdatedListener.Event event = + new FakeGitReferenceUpdatedEvent( + project, + sourceRef, + ObjectId.zeroId().getName(), + sourceCommit.getId().getName(), + ReceiveCommand.Type.CREATE); + pullReplicationQueue.onGitReferenceUpdated(event); + + try (Repository repo = repoManager.openRepository(project)) { + waitUntil(() -> checkedGetRef(repo, sourceRef) != null); + + Ref targetBranchRef = getRef(repo, sourceRef); + assertThat(targetBranchRef).isNotNull(); + assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId()); + } + } +}
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 1019b86..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,18 +42,18 @@ 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.AuthScope; +import org.apache.http.auth.AuthenticationException; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.CredentialsProvider; import org.apache.http.client.ResponseHandler; 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.protocol.HttpClientContext; +import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.auth.BasicScheme; import org.apache.http.message.BasicHeader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.storage.file.FileBasedConfig; @@ -83,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 { @@ -109,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) { @@ -170,17 +174,28 @@ }; } - protected HttpClientContext getContext() { - return getContextForAccount(admin); + protected HttpRequestBase withBearerTokenAuthentication( + HttpRequestBase httpRequest, String bearerToken) { + httpRequest.addHeader(new BasicHeader(HttpHeaders.AUTHORIZATION, "Bearer " + bearerToken)); + return httpRequest; } - protected HttpClientContext getUserContext() { - return getContextForAccount(user); + protected HttpRequestBase withBasicAuthenticationAsAdmin(HttpRequestBase httpRequest) + throws AuthenticationException { + return withBasicAuthentication(httpRequest, admin); } - protected HttpClientContext getAnonymousContext() { - HttpClientContext ctx = HttpClientContext.create(); - return ctx; + protected HttpRequestBase withBasicAuthenticationAsUser(HttpRequestBase httpRequest) + throws AuthenticationException { + return withBasicAuthentication(httpRequest, user); + } + + private HttpRequestBase withBasicAuthentication(HttpRequestBase httpRequest, TestAccount account) + throws AuthenticationException { + UsernamePasswordCredentials creds = + new UsernamePasswordCredentials(account.username(), account.httpPassword()); + httpRequest.addHeader(new BasicScheme().authenticate(creds, httpRequest, null)); + return httpRequest; } private Project.NameKey createTestProject(String name) throws Exception { @@ -200,7 +215,6 @@ replicaSuffixes.stream() .map(suffix -> gitPath.resolve("${name}" + suffix + ".git").toString()) .collect(toList()); - config.setString("replication", null, "instanceLabel", remoteName); config.setStringList("remote", remoteName, "url", replicaUrls); config.setString("remote", remoteName, "apiUrl", adminRestSession.url()); config.setString("remote", remoteName, "fetch", "+refs/tags/*:refs/tags/*"); @@ -217,13 +231,4 @@ secureConfig.setString("remote", remoteName, "password", password); secureConfig.save(); } - - private HttpClientContext getContextForAccount(TestAccount account) { - HttpClientContext ctx = HttpClientContext.create(); - CredentialsProvider adapted = new BasicCredentialsProvider(); - adapted.setCredentials( - AuthScope.ANY, new UsernamePasswordCredentials(account.username(), account.httpPassword())); - ctx.setCredentialsProvider(adapted); - return ctx; - } }
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 aad3903..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
@@ -22,7 +22,6 @@ import com.google.gerrit.extensions.restapi.Url; import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData; import java.util.Optional; -import org.apache.http.client.methods.HttpPost; import org.junit.Test; public class ApplyObjectActionIT extends ActionITBase { @@ -41,8 +40,11 @@ RevisionData revisionData = revisionDataOption.get(); String sendObjectPayload = createPayload(payloadWithAsyncFieldTemplate, refName, revisionData); - HttpPost post = createRequest(sendObjectPayload); - httpClientFactory.create(source).execute(post, assertHttpResponseCode(201), getContext()); + httpClientFactory + .create(source) + .execute( + withBasicAuthenticationAsAdmin(createRequest(sendObjectPayload)), + assertHttpResponseCode(201)); } @Test @@ -60,8 +62,11 @@ String sendObjectPayload = createPayload(payloadWithoutAsyncFieldTemplate, refName, revisionData); - HttpPost post = createRequest(sendObjectPayload); - httpClientFactory.create(source).execute(post, assertHttpResponseCode(201), getContext()); + httpClientFactory + .create(source) + .execute( + withBasicAuthenticationAsAdmin(createRequest(sendObjectPayload)), + assertHttpResponseCode(201)); } @Test @@ -80,8 +85,11 @@ String sendObjectPayload = createPayload(payloadWithoutAsyncFieldTemplate, refName, revisionData); - HttpPost post = createRequest(sendObjectPayload); - httpClientFactory.create(source).execute(post, assertHttpResponseCode(201), getContext()); + httpClientFactory + .create(source) + .execute( + withBasicAuthenticationAsAdmin(createRequest(sendObjectPayload)), + assertHttpResponseCode(201)); } @Test @@ -103,13 +111,16 @@ String.format( "%s/a/projects/%s/pull-replication~apply-object", adminRestSession.url(), Url.encode(projectName.get())); - HttpPost post = createRequest(sendObjectPayload); - httpClientFactory.create(source).execute(post, assertHttpResponseCode(201), getContext()); + httpClientFactory + .create(source) + .execute( + withBasicAuthenticationAsAdmin(createRequest(sendObjectPayload)), + assertHttpResponseCode(201)); } @Test @GerritConfig(name = "container.replica", value = "true") - public void shouldReturnUnauthorizedWhenNodeIsAReplicaAndUSerIsAnonymous() throws Exception { + public void shouldReturnForbiddenWhenNodeIsAReplicaAndUSerIsAnonymous() throws Exception { String payloadWithoutAsyncFieldTemplate = "{\"label\":\"" + TEST_REPLICATION_REMOTE @@ -123,10 +134,9 @@ String sendObjectPayload = createPayload(payloadWithoutAsyncFieldTemplate, refName, revisionData); - HttpPost post = createRequest(sendObjectPayload); httpClientFactory .create(source) - .execute(post, assertHttpResponseCode(401), getAnonymousContext()); + .execute(createRequest(sendObjectPayload), assertHttpResponseCode(403)); } @Test @@ -142,8 +152,11 @@ String sendObjectPayload = createPayload(payloadWithoutLabelFieldTemplate, refName, revisionData); - HttpPost post = createRequest(sendObjectPayload); - httpClientFactory.create(source).execute(post, assertHttpResponseCode(400), getContext()); + httpClientFactory + .create(source) + .execute( + withBasicAuthenticationAsAdmin(createRequest(sendObjectPayload)), + assertHttpResponseCode(400)); } @Test @@ -160,8 +173,61 @@ RevisionData revisionData = revisionDataOption.get(); String sendObjectPayload = createPayload(wrongPayloadTemplate, refName, revisionData); - HttpPost post = createRequest(sendObjectPayload); - httpClientFactory.create(source).execute(post, assertHttpResponseCode(400), getContext()); + httpClientFactory + .create(source) + .execute( + withBasicAuthenticationAsAdmin(createRequest(sendObjectPayload)), + 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( @@ -176,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..824496a --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
@@ -0,0 +1,212 @@ +// 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.atMost; +import static org.mockito.Mockito.times; +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.util.ThreadLocalRequestContext; +import com.google.inject.Provider; +import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationInternalUser; +import java.io.IOException; +import java.util.Optional; +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 { + + private final Optional<String> NO_QUERY_PARAMETERS = Optional.empty(); + private final Optional<String> GIT_UPLOAD_PACK_QUERY_PARAMETER = + Optional.of("service=git-upload-pack"); + @Mock private DynamicItem<WebSession> session; + @Mock private WebSession webSession; + @Mock private Provider<ThreadLocalRequestContext> threadLocalRequestContextProvider; + @Mock private PullReplicationInternalUser 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 authenticateAndFilter(String uri, Optional<String> queryStringMaybe) + throws ServletException, IOException { + final String bearerToken = "some-bearer-token"; + when(httpServletRequest.getRequestURI()).thenReturn(uri); + queryStringMaybe.ifPresent(qs -> when(httpServletRequest.getQueryString()).thenReturn(qs)); + when(httpServletRequest.getHeader("Authorization")) + .thenReturn(String.format("Bearer %s", bearerToken)); + when(threadLocalRequestContextProvider.get()).thenReturn(threadLocalRequestContext); + when(session.get()).thenReturn(webSession); + final BearerAuthenticationFilter filter = + new BearerAuthenticationFilter( + session, pluginName, pluginUser, threadLocalRequestContextProvider, bearerToken); + filter.doFilter(httpServletRequest, httpServletResponse, filterChain); + + verify(httpServletRequest, atMost(2)).getRequestURI(); + verify(httpServletRequest, atMost(1)).getQueryString(); + verify(httpServletRequest).getHeader("Authorization"); + verify(threadLocalRequestContextProvider).get(); + verify(session).get(); + verify(webSession).setAccessPathOk(AccessPath.REST_API, true); + verify(filterChain).doFilter(httpServletRequest, httpServletResponse); + } + + @Test + public void shouldAuthenticateWhenFetch() throws ServletException, IOException { + authenticateAndFilter("any-prefix/pull-replication~fetch", NO_QUERY_PARAMETERS); + } + + @Test + public void shouldAuthenticateWhenApplyObject() throws ServletException, IOException { + authenticateAndFilter("any-prefix/pull-replication~apply-object", NO_QUERY_PARAMETERS); + } + + @Test + public void shouldAuthenticateWhenApplyObjects() throws ServletException, IOException { + authenticateAndFilter("any-prefix/pull-replication~apply-objects", NO_QUERY_PARAMETERS); + } + + @Test + public void shouldAuthenticateWhenDeleteProject() throws ServletException, IOException { + authenticateAndFilter("any-prefix/pull-replication~delete-project", NO_QUERY_PARAMETERS); + } + + @Test + public void shouldAuthenticateWhenUpdateHead() throws ServletException, IOException { + authenticateAndFilter("any-prefix/projects/my-project/HEAD", NO_QUERY_PARAMETERS); + } + + @Test + public void shouldAuthenticateWhenInitProject() throws ServletException, IOException { + authenticateAndFilter( + "any-prefix/pull-replication/init-project/my-project.git", NO_QUERY_PARAMETERS); + } + + @Test + public void shouldAuthenticateWhenGitUploadPacket() throws ServletException, IOException { + authenticateAndFilter("any-prefix/git-upload-pack", NO_QUERY_PARAMETERS); + } + + @Test + public void shouldAuthenticateWhenGitUploadPacketInQueryParameter() + throws ServletException, IOException { + authenticateAndFilter("any-prefix", GIT_UPLOAD_PACK_QUERY_PARAMETER); + } + + @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, + pluginUser, + 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, + pluginUser, + 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, + pluginUser, + 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, + pluginUser, + threadLocalRequestContextProvider, + "some-bearer-token"); + filter.doFilter(httpServletRequest, httpServletResponse, filterChain); + + verify(httpServletRequest, times(2)).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, + pluginUser, + 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/DeleteRefCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java index 4415a4b..eb4d322 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java
@@ -33,6 +33,7 @@ import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectState; import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent; +import com.googlesource.gerrit.plugins.replication.pull.LocalGitRepositoryManagerProvider; import com.googlesource.gerrit.plugins.replication.pull.PullReplicationStateLogger; import com.googlesource.gerrit.plugins.replication.pull.fetch.ApplyObject; import java.util.Optional; @@ -94,7 +95,7 @@ applyObject, permissionBackend, eventDispatcherDataItem, - gitManager); + new LocalGitRepositoryManagerProvider(gitManager)); } @Test
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 4ad8ce6..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
@@ -35,7 +35,9 @@ httpClientFactory .create(source) - .execute(createRequest(sendObjectPayload), assertHttpResponseCode(201), getContext()); + .execute( + withBasicAuthenticationAsAdmin(createRequest(sendObjectPayload)), + assertHttpResponseCode(201)); } @Test @@ -55,12 +57,14 @@ adminRestSession.url(), Url.encode(projectName.get())); httpClientFactory .create(source) - .execute(createRequest(sendObjectPayload), assertHttpResponseCode(201), getContext()); + .execute( + withBasicAuthenticationAsAdmin(createRequest(sendObjectPayload)), + assertHttpResponseCode(201)); } @Test @GerritConfig(name = "container.replica", value = "true") - public void shouldReturnUnauthorizedWhenNodeIsAReplicaAndUSerIsAnonymous() throws Exception { + public void shouldReturnForbiddenWhenNodeIsAReplicaAndUSerIsAnonymous() throws Exception { String refName = createRef(); String sendObjectPayload = "{\"label\":\"" @@ -71,12 +75,51 @@ httpClientFactory .create(source) + .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( - createRequest(sendObjectPayload), assertHttpResponseCode(401), getAnonymousContext()); + 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/FetchActionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java index fb7f3d1..ce0b9d3 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
@@ -55,6 +55,8 @@ int taskId = 1234; @Mock FetchCommand fetchCommand; + @Mock FetchJob fetchJob; + @Mock FetchJob.Factory fetchJobFactory; @Mock ProjectResource projectResource; @Mock WorkQueue workQueue; @Mock ScheduledExecutorService exceutorService; @@ -65,6 +67,7 @@ @Before public void setup() { + when(fetchJobFactory.create(any(), any(), any())).thenReturn(fetchJob); when(workQueue.getDefaultQueue()).thenReturn(exceutorService); when(urlFormatter.getRestUrl(anyString())).thenReturn(Optional.of(location)); when(exceutorService.submit(any(Runnable.class))) @@ -79,7 +82,9 @@ when(task.getTaskId()).thenReturn(taskId); when(preConditions.canCallFetchApi()).thenReturn(true); - fetchAction = new FetchAction(fetchCommand, workQueue, urlFormatterDynamicItem, preConditions); + fetchAction = + new FetchAction( + fetchCommand, workQueue, urlFormatterDynamicItem, preConditions, fetchJobFactory); } @Test
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 56a397e..a46a721 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
@@ -22,6 +22,7 @@ import com.google.gerrit.server.group.SystemGroupBackend; 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; @@ -36,21 +37,18 @@ httpClientFactory .create(source) .execute( - createDeleteRequest(), - assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED), - getAnonymousContext()); + createDeleteRequest(), assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED)); } @Test public void shouldDeleteRepositoryWhenUserHasProjectDeletionCapabilities() throws Exception { String testProjectName = project.get(); - url = getURL(testProjectName); + url = getURLWithAuthenticationPrefix(testProjectName); httpClientFactory .create(source) .execute( - createDeleteRequest(), - assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN), - getUserContext()); + withBasicAuthenticationAsUser(createDeleteRequest()), + assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN)); projectOperations .project(allProjects) @@ -61,56 +59,53 @@ httpClientFactory .create(source) .execute( - createDeleteRequest(), - assertHttpResponseCode(HttpServletResponse.SC_OK), - getUserContext()); + withBasicAuthenticationAsUser(createDeleteRequest()), + assertHttpResponseCode(HttpServletResponse.SC_OK)); } @Test public void shouldReturnOKWhenProjectIsDeleted() throws Exception { String testProjectName = project.get(); - url = getURL(testProjectName); + url = getURLWithAuthenticationPrefix(testProjectName); httpClientFactory .create(source) .execute( - createDeleteRequest(), assertHttpResponseCode(HttpServletResponse.SC_OK), getContext()); + withBasicAuthenticationAsAdmin(createDeleteRequest()), + assertHttpResponseCode(HttpServletResponse.SC_OK)); } @Test @Ignore("Failing in RestApiServlet: to be enabled again once that is fixed in core") public void shouldReturnBadRequestWhenDeletingAnInvalidProjectName() throws Exception { - url = getURL(INVALID_TEST_PROJECT_NAME); + url = getURLWithAuthenticationPrefix(INVALID_TEST_PROJECT_NAME); httpClientFactory .create(source) .execute( - createDeleteRequest(), - assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST), - getContext()); + withBasicAuthenticationAsAdmin(createDeleteRequest()), + assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST)); } @Test @GerritConfig(name = "container.replica", value = "true") - public void shouldReturnUnauthorizedForUserWithoutPermissionsOnReplica() throws Exception { + public void shouldReturnForbiddenForUserWithoutPermissionsOnReplica() throws Exception { httpClientFactory .create(source) - .execute( - createDeleteRequest(), - assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED), - getAnonymousContext()); + .execute(createDeleteRequest(), assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN)); } @Test @GerritConfig(name = "container.replica", value = "true") public void shouldReturnOKWhenProjectIsDeletedOnReplica() throws Exception { String testProjectName = project.get(); - url = getURL(testProjectName); + url = getURLWithAuthenticationPrefix(testProjectName); httpClientFactory .create(source) .execute( - createDeleteRequest(), assertHttpResponseCode(HttpServletResponse.SC_OK), getContext()); + withBasicAuthenticationAsAdmin(createDeleteRequest()), + assertHttpResponseCode(HttpServletResponse.SC_OK)); } @Test @@ -118,13 +113,12 @@ public void shouldDeleteRepositoryWhenUserHasProjectDeletionCapabilitiesAndNodeIsAReplica() throws Exception { String testProjectName = project.get(); - url = getURL(testProjectName); + url = getURLWithAuthenticationPrefix(testProjectName); + HttpRequestBase deleteRequest = withBasicAuthenticationAsUser(createDeleteRequest()); + httpClientFactory .create(source) - .execute( - createDeleteRequest(), - assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN), - getUserContext()); + .execute(deleteRequest, assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN)); projectOperations .project(allProjects) @@ -134,28 +128,52 @@ httpClientFactory .create(source) - .execute( - createDeleteRequest(), - assertHttpResponseCode(HttpServletResponse.SC_OK), - getUserContext()); + .execute(deleteRequest, assertHttpResponseCode(HttpServletResponse.SC_OK)); } @Test @GerritConfig(name = "container.replica", value = "true") public void shouldReturnBadRequestWhenDeletingAnInvalidProjectNameWhenNodeIsAReplica() throws Exception { - url = getURL(INVALID_TEST_PROJECT_NAME); + url = getURLWithAuthenticationPrefix(INVALID_TEST_PROJECT_NAME); httpClientFactory .create(source) .execute( - createDeleteRequest(), - assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST), - getContext()); + withBasicAuthenticationAsAdmin(createDeleteRequest()), + assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST)); + } + + @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 d8ff02a..77f05a1 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; @@ -27,6 +26,7 @@ import javax.servlet.http.HttpServletResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.message.BasicHeader; import org.junit.Test; @@ -40,8 +40,7 @@ .create(source) .execute( createPutRequestWithHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED), - getAnonymousContext()); + assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED)); } @Test @@ -49,40 +48,37 @@ httpClientFactory .create(source) .execute( - createPutRequestWithoutHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST), - getContext()); + withBasicAuthenticationAsAdmin(createPutRequestWithoutHeaders()), + assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST)); } @Test public void shouldCreateRepository() throws Exception { String newProjectName = "new/newProjectForPrimary"; - url = getURL(newProjectName); + url = getURLWithAuthenticationPrefix(newProjectName); httpClientFactory .create(source) .execute( - createPutRequestWithHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_CREATED), - getContext()); + withBasicAuthenticationAsAdmin(createPutRequestWithHeaders()), + assertHttpResponseCode(HttpServletResponse.SC_CREATED)); - HttpGet getNewProjectRequest = - new HttpGet(userRestSession.url() + "/a/projects/" + Url.encode(newProjectName)); + HttpRequestBase getNewProjectRequest = + withBasicAuthenticationAsAdmin( + new HttpGet(userRestSession.url() + "/a/projects/" + Url.encode(newProjectName))); + httpClientFactory .create(source) - .execute( - getNewProjectRequest, assertHttpResponseCode(HttpServletResponse.SC_OK), getContext()); + .execute(getNewProjectRequest, assertHttpResponseCode(HttpServletResponse.SC_OK)); } @Test public void shouldCreateRepositoryWhenUserHasProjectCreationCapabilities() throws Exception { String newProjectName = "new/newProjectForUserWithCapabilities"; - url = getURL(newProjectName); + url = getURLWithAuthenticationPrefix(newProjectName); + HttpRequestBase put = withBasicAuthenticationAsUser(createPutRequestWithHeaders()); httpClientFactory .create(source) - .execute( - createPutRequestWithHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN), - getUserContext()); + .execute(put, assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN)); projectOperations .project(allProjects) @@ -94,10 +90,7 @@ httpClientFactory .create(source) - .execute( - createPutRequestWithHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_CREATED), - getUserContext()); + .execute(put, assertHttpResponseCode(HttpServletResponse.SC_CREATED)); } @Test @@ -105,22 +98,20 @@ httpClientFactory .create(source) .execute( - createPutRequestWithHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN), - getUserContext()); + withBasicAuthenticationAsUser(createPutRequestWithHeaders()), + assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN)); } @Test @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( - createPutRequestWithHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_CREATED), - getContext()); + withBasicAuthenticationAsAdmin(createPutRequestWithHeaders()), + assertHttpResponseCode(HttpServletResponse.SC_CREATED)); } @Test @@ -129,9 +120,8 @@ httpClientFactory .create(source) .execute( - createPutRequestWithHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN), - getUserContext()); + withBasicAuthenticationAsUser(createPutRequestWithHeaders()), + assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN)); } @Test @@ -139,13 +129,11 @@ public void shouldCreateRepositoryWhenUserHasProjectCreationCapabilitiesAndNodeIsAReplica() throws Exception { String newProjectName = "new/newProjectForUserWithCapabilitiesReplica"; - url = getURL(newProjectName); + url = getURLWithAuthenticationPrefix(newProjectName); + HttpRequestBase put = withBasicAuthenticationAsUser(createPutRequestWithHeaders()); httpClientFactory .create(source) - .execute( - createPutRequestWithHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN), - getUserContext()); + .execute(put, assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN)); projectOperations .project(allProjects) @@ -157,24 +145,19 @@ httpClientFactory .create(source) - .execute( - createPutRequestWithHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_CREATED), - getUserContext()); + .execute(put, assertHttpResponseCode(HttpServletResponse.SC_CREATED)); } @Test @GerritConfig(name = "container.replica", value = "true") public void shouldReturnBadRequestIfProjectNameIsInvalidAndCannotBeCreatedWhenNodeIsAReplica() throws Exception { - url = getURL(INVALID_TEST_PROJECT_NAME); - + url = getURLWithAuthenticationPrefix(INVALID_TEST_PROJECT_NAME); httpClientFactory .create(source) .execute( - createPutRequestWithHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST), - getContext()); + withBasicAuthenticationAsAdmin(createPutRequestWithHeaders()), + assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST)); } @Test @@ -183,28 +166,50 @@ httpClientFactory .create(source) .execute( - createPutRequestWithoutHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST), - getContext()); + withBasicAuthenticationAsAdmin(createPutRequestWithoutHeaders()), + assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST)); } @Test @GerritConfig(name = "container.replica", value = "true") - public void shouldReturnUnauthorizedForUserWithoutPermissionsWhenNodeIsAReplica() - throws Exception { + public void shouldReturnForbiddenForUserWithoutPermissionsWhenNodeIsAReplica() throws Exception { httpClientFactory .create(source) .execute( createPutRequestWithHeaders(), - assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED), - getAnonymousContext()); + 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/PullReplicationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java new file mode 100644 index 0000000..2ed1466 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
@@ -0,0 +1,331 @@ +package com.googlesource.gerrit.plugins.replication.pull.api; + +import static com.google.common.net.HttpHeaders.ACCEPT; +import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_UNPROCESSABLE_ENTITY; +import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.atLeastOnce; +import static org.mockito.internal.verification.VerificationModeFactory.times; + +import com.google.common.net.MediaType; +import com.google.gerrit.extensions.restapi.*; +import com.google.gerrit.server.project.ProjectResource; +import com.google.gerrit.server.restapi.project.ProjectsCollection; +import java.io.*; +import java.nio.charset.StandardCharsets; +import javax.servlet.FilterChain; +import javax.servlet.ServletOutputStream; +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 PullReplicationFilterTest { + + @Mock HttpServletRequest request; + @Mock HttpServletResponse response; + @Mock FilterChain filterChain; + @Mock private FetchAction fetchAction; + @Mock private ApplyObjectAction applyObjectAction; + @Mock private ApplyObjectsAction applyObjectsAction; + @Mock private ProjectInitializationAction projectInitializationAction; + @Mock private UpdateHeadAction updateHEADAction; + @Mock private ProjectDeletionAction projectDeletionAction; + @Mock private ProjectsCollection projectsCollection; + @Mock private ProjectResource projectResource; + @Mock private ServletOutputStream outputStream; + @Mock private PrintWriter printWriter; + private final String PLUGIN_NAME = "pull-replication"; + private final String PROJECT_NAME = "some-project"; + private final String PROJECT_NAME_GIT = "some-project.git"; + private final String FETCH_URI = + String.format("any-prefix/projects/%s/%s~fetch", PROJECT_NAME, PLUGIN_NAME); + private final String APPLY_OBJECT_URI = + String.format("any-prefix/projects/%s/%s~apply-object", PROJECT_NAME, PLUGIN_NAME); + private final String APPLY_OBJECTS_URI = + String.format("any-prefix/projects/%s/%s~apply-objects", PROJECT_NAME, PLUGIN_NAME); + private final String HEAD_URI = String.format("any-prefix/projects/%s/HEAD", PROJECT_NAME); + private final String DELETE_PROJECT_URI = + String.format("any-prefix/projects/%s/%s~delete-project", PROJECT_NAME, PLUGIN_NAME); + private final String INIT_PROJECT_URI = + String.format("any-prefix/%s/init-project/%s", PLUGIN_NAME, PROJECT_NAME_GIT); + + private final Response OK_RESPONSE = Response.ok(); + + private PullReplicationFilter createPullReplicationFilter() { + return new PullReplicationFilter( + fetchAction, + applyObjectAction, + applyObjectsAction, + projectInitializationAction, + updateHEADAction, + projectDeletionAction, + projectsCollection, + PLUGIN_NAME); + } + + private void defineBehaviours(byte[] payload, String uri) throws Exception { + when(request.getRequestURI()).thenReturn(uri); + InputStream is = new ByteArrayInputStream(payload); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is)); + when(request.getReader()).thenReturn(bufferedReader); + when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME))) + .thenReturn(projectResource); + when(response.getWriter()).thenReturn(printWriter); + } + + private void verifyBehaviours() throws Exception { + verify(request, atLeastOnce()).getRequestURI(); + verify(request).getReader(); + verify(projectsCollection).parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)); + verify(response).getWriter(); + verify(response).setContentType("application/json"); + verify(response).setStatus(HttpServletResponse.SC_OK); + } + + @Test + public void shouldFilterFetchAction() throws Exception { + byte[] payloadFetch = + ("{" + + "\"label\":\"Replication\", " + + "\"ref_name\": \"refs/heads/master\", " + + "\"async\":false" + + "}") + .getBytes(StandardCharsets.UTF_8); + + defineBehaviours(payloadFetch, FETCH_URI); + when(fetchAction.apply(any(), any())).thenReturn(OK_RESPONSE); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verifyBehaviours(); + verify(fetchAction).apply(eq(projectResource), any()); + } + + @Test + public void shouldFilterApplyObjectAction() throws Exception { + + byte[] payloadApplyObject = + ("{\"label\":\"Replication\",\"ref_name\":\"refs/heads/master\"," + + "\"revision_data\":{" + + "\"commit_object\":{\"type\":1,\"content\":\"some-content\"}," + + "\"tree_object\":{\"type\":2,\"content\":\"some-content\"}," + + "\"blobs\":[]}" + + "}") + .getBytes(StandardCharsets.UTF_8); + + defineBehaviours(payloadApplyObject, APPLY_OBJECT_URI); + + when(applyObjectAction.apply(any(), any())).thenReturn(OK_RESPONSE); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verifyBehaviours(); + verify(applyObjectAction).apply(eq(projectResource), any()); + } + + @Test + public void shouldFilterApplyObjectsAction() throws Exception { + + byte[] payloadApplyObjects = + ("{\"label\":\"Replication\",\"ref_name\":\"refs/heads/master\"," + + "\"revisions_data\":[{" + + "\"commit_object\":{\"type\":1,\"content\":\"some-content\"}," + + "\"tree_object\":{\"type\":2,\"content\":\"some-content\"}," + + "\"blobs\":[]}]}") + .getBytes(StandardCharsets.UTF_8); + + defineBehaviours(payloadApplyObjects, APPLY_OBJECTS_URI); + + when(applyObjectsAction.apply(any(), any())).thenReturn(OK_RESPONSE); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verifyBehaviours(); + verify(applyObjectsAction).apply(eq(projectResource), any()); + } + + @Test + public void shouldFilterProjectInitializationAction() throws Exception { + + when(request.getRequestURI()).thenReturn(INIT_PROJECT_URI); + when(request.getHeader(ACCEPT)).thenReturn(MediaType.PLAIN_TEXT_UTF_8.toString()); + when(projectInitializationAction.initProject(PROJECT_NAME_GIT)).thenReturn(true); + when(response.getWriter()).thenReturn(printWriter); + + final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(request, times(5)).getRequestURI(); + verify(projectInitializationAction).initProject(eq(PROJECT_NAME_GIT)); + verify(response).getWriter(); + } + + @Test + public void shouldFilterUpdateHEADAction() throws Exception { + + byte[] payloadUpdateHead = "{\"ref\":\"some-ref\"}".getBytes(StandardCharsets.UTF_8); + defineBehaviours(payloadUpdateHead, HEAD_URI); + when(request.getMethod()).thenReturn("PUT"); + when(updateHEADAction.apply(any(), any())).thenReturn(OK_RESPONSE); + + final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verifyBehaviours(); + verify(updateHEADAction).apply(eq(projectResource), any()); + } + + @Test + public void shouldFilterProjectDeletionAction() throws Exception { + when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI); + when(request.getMethod()).thenReturn("DELETE"); + when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME))) + .thenReturn(projectResource); + when(projectDeletionAction.apply(any(), any())).thenReturn(OK_RESPONSE); + when(response.getWriter()).thenReturn(printWriter); + + final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(request, times(7)).getRequestURI(); + verify(projectsCollection).parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)); + verify(projectDeletionAction).apply(eq(projectResource), any()); + verify(response).getWriter(); + verify(response).setContentType("application/json"); + verify(response).setStatus(OK_RESPONSE.statusCode()); + } + + @Test + public void shouldGoNextInChainWhenUriDoesNotMatch() throws Exception { + when(request.getRequestURI()).thenReturn("any-url"); + final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + verify(filterChain).doFilter(request, response); + } + + @Test + public void shouldBe404WhenJsonIsMalformed() throws Exception { + byte[] payloadMalformedJson = "some-json-malformed".getBytes(StandardCharsets.UTF_8); + InputStream is = new ByteArrayInputStream(payloadMalformedJson); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is)); + when(request.getRequestURI()).thenReturn(FETCH_URI); + when(request.getReader()).thenReturn(bufferedReader); + when(response.getOutputStream()).thenReturn(outputStream); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + + @Test + public void shouldBe500WhenProjectCannotBeInitiated() throws Exception { + when(request.getRequestURI()).thenReturn(INIT_PROJECT_URI); + when(request.getHeader(ACCEPT)).thenReturn(MediaType.PLAIN_TEXT_UTF_8.toString()); + when(projectInitializationAction.initProject(PROJECT_NAME_GIT)).thenReturn(false); + when(response.getOutputStream()).thenReturn(outputStream); + + final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void shouldBe500WhenResourceNotFound() throws Exception { + when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI); + when(request.getMethod()).thenReturn("DELETE"); + when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME))) + .thenReturn(projectResource); + when(projectDeletionAction.apply(any(), any())) + .thenThrow(new ResourceNotFoundException("resource not found")); + when(response.getOutputStream()).thenReturn(outputStream); + + final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void shouldBe403WhenUserIsNotAuthorised() throws Exception { + byte[] payloadFetchAction = + ("{" + + "\"label\":\"Replication\", " + + "\"ref_name\": \"refs/heads/master\", " + + "\"async\":false" + + "}") + .getBytes(StandardCharsets.UTF_8); + + defineBehaviours(payloadFetchAction, FETCH_URI); + when(fetchAction.apply(any(), any())) + .thenThrow(new AuthException("The user is not authorised")); + when(response.getOutputStream()).thenReturn(outputStream); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(HttpServletResponse.SC_FORBIDDEN); + } + + @Test + public void shouldBe422WhenEntityCannotBeProcessed() throws Exception { + byte[] payloadFetchAction = + ("{" + + "\"label\":\"Replication\", " + + "\"ref_name\": \"refs/heads/master\", " + + "\"async\":false" + + "}") + .getBytes(StandardCharsets.UTF_8); + + defineBehaviours(payloadFetchAction, FETCH_URI); + when(fetchAction.apply(any(), any())) + .thenThrow(new UnprocessableEntityException("Entity cannot be processed")); + when(response.getOutputStream()).thenReturn(outputStream); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(SC_UNPROCESSABLE_ENTITY); + } + + @Test + public void shouldBe409WhenThereIsResourceConflict() throws Exception { + when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI); + when(request.getMethod()).thenReturn("DELETE"); + when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME))) + .thenReturn(projectResource); + + when(projectDeletionAction.apply(any(), any())) + .thenThrow(new ResourceConflictException("Resource conflict")); + when(response.getOutputStream()).thenReturn(outputStream); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(SC_CONFLICT); + } + + @Test + public void shouldBe400WhenProjectNameIsNotPresentInURL() throws Exception { + when(request.getRequestURI()) + .thenReturn(String.format("any-prefix/projects/%s~delete-project", PLUGIN_NAME)); + when(request.getMethod()).thenReturn("DELETE"); + when(response.getOutputStream()).thenReturn(outputStream); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(HttpServletResponse.SC_BAD_REQUEST); + } +}
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 aa07a7c..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
@@ -26,6 +26,8 @@ import com.google.gson.Gson; 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 { @@ -39,8 +41,7 @@ .create(source) .execute( createPutRequest(headInput("some/branch")), - assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED), - getAnonymousContext()); + assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED)); } @Test @@ -48,9 +49,8 @@ httpClientFactory .create(source) .execute( - createPutRequest(headInput("")), - assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST), - getContext()); + withBasicAuthenticationAsAdmin(createPutRequest(headInput(""))), + assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST)); } @Test @@ -61,13 +61,11 @@ BranchInput input = new BranchInput(); input.revision = master; gApi.projects().name(testProjectName).branch(newBranch).create(input); - httpClientFactory .create(source) .execute( - createPutRequest(headInput(newBranch)), - assertHttpResponseCode(HttpServletResponse.SC_OK), - getContext()); + withBasicAuthenticationAsAdmin(createPutRequest(headInput(newBranch))), + assertHttpResponseCode(HttpServletResponse.SC_OK)); assertThat(gApi.projects().name(testProjectName).head()).isEqualTo(newBranch); } @@ -78,9 +76,8 @@ httpClientFactory .create(source) .execute( - createPutRequest(headInput("")), - assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST), - getContext()); + withBasicAuthenticationAsAdmin(createPutRequest(headInput(""))), + assertHttpResponseCode(HttpServletResponse.SC_BAD_REQUEST)); } @Test @@ -92,13 +89,11 @@ BranchInput input = new BranchInput(); input.revision = master; gApi.projects().name(testProjectName).branch(newBranch).create(input); - httpClientFactory .create(source) .execute( - createPutRequest(headInput(newBranch)), - assertHttpResponseCode(HttpServletResponse.SC_OK), - getContext()); + withBasicAuthenticationAsAdmin(createPutRequest(headInput(newBranch))), + assertHttpResponseCode(HttpServletResponse.SC_OK)); assertThat(gApi.projects().name(testProjectName).head()).isEqualTo(newBranch); } @@ -108,9 +103,8 @@ httpClientFactory .create(source) .execute( - createPutRequest(headInput("some/new/head")), - assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN), - getUserContext()); + withBasicAuthenticationAsUser(createPutRequest(headInput("some/new/head"))), + assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN)); } @Test @@ -121,13 +115,10 @@ BranchInput input = new BranchInput(); input.revision = master; gApi.projects().name(testProjectName).branch(newBranch).create(input); - + HttpRequestBase put = withBasicAuthenticationAsUser(createPutRequest(headInput(newBranch))); httpClientFactory .create(source) - .execute( - createPutRequest(headInput(newBranch)), - assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN), - getUserContext()); + .execute(put, assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN)); projectOperations .project(project) @@ -137,10 +128,7 @@ httpClientFactory .create(source) - .execute( - createPutRequest(headInput(newBranch)), - assertHttpResponseCode(HttpServletResponse.SC_OK), - getUserContext()); + .execute(put, assertHttpResponseCode(HttpServletResponse.SC_OK)); } @Test @@ -149,9 +137,52 @@ httpClientFactory .create(source) .execute( - createPutRequest(headInput("some/new/head")), - assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN), - getUserContext()); + withBasicAuthenticationAsUser(createPutRequest(headInput("some/new/head"))), + 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) { @@ -161,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/auth/PullReplicationGroupBackendIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackendIT.java new file mode 100644 index 0000000..87738e3 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackendIT.java
@@ -0,0 +1,80 @@ +// 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.auth; + +import static com.google.common.truth.Truth.assertThat; +import static com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupBackend.INTERNAL_GROUP_DESCRIPTION; +import static com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupBackend.INTERNAL_GROUP_NAME; +import static com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupBackend.INTERNAL_GROUP_UUID; +import static com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupBackend.NAME_PREFIX; + +import com.google.gerrit.acceptance.LightweightPluginDaemonTest; +import com.google.gerrit.acceptance.SkipProjectClone; +import com.google.gerrit.acceptance.TestPlugin; +import com.google.gerrit.entities.GroupDescription; +import com.google.gerrit.entities.GroupReference; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.GroupMembership; +import com.google.gerrit.server.group.SystemGroupBackend; +import java.util.Collection; +import org.junit.Test; + +@SkipProjectClone +@TestPlugin( + name = "pull-replication", + sysModule = "com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupModule") +public class PullReplicationGroupBackendIT extends LightweightPluginDaemonTest { + + @Test + public void shouldResolvePullReplicationInternalGroup() { + GroupDescription.Basic group = groupBackend.get(INTERNAL_GROUP_UUID); + + assertThat(group).isNotNull(); + assertThat(group).isEqualTo(INTERNAL_GROUP_DESCRIPTION); + } + + @Test + public void shouldSuggestPullReplicationInternalGroup() { + Collection<GroupReference> groups = groupBackend.suggest(NAME_PREFIX, null); + + assertThat(groups).isNotNull(); + assertThat(groups).hasSize(1); + + GroupReference groupReference = groups.iterator().next(); + assertThat(groupReference.getName()).isEqualTo(INTERNAL_GROUP_NAME); + assertThat(groupReference.getUUID()).isEqualTo(INTERNAL_GROUP_UUID); + } + + @Test + public void pullReplicationInternalUserShouldHaveMembershipOfInternalGroupAndAnonymousUsers() { + assertMemberOfInternalAndAnonymousUsers( + groupBackend.membershipsOf(getPullReplicationInternalUser())); + } + + @Test + public void pullReplicationInternalUserShouldHaveEffectiveGroups() { + assertMemberOfInternalAndAnonymousUsers(getPullReplicationInternalUser().getEffectiveGroups()); + } + + private CurrentUser getPullReplicationInternalUser() { + CurrentUser user = plugin.getSysInjector().getInstance(PullReplicationInternalUser.class); + return user; + } + + private void assertMemberOfInternalAndAnonymousUsers(GroupMembership userMembership) { + assertThat(userMembership.contains(INTERNAL_GROUP_UUID)).isTrue(); + assertThat(userMembership.contains(SystemGroupBackend.ANONYMOUS_USERS)).isTrue(); + } +}
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 82% 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 c45e7be..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,13 +66,13 @@ @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 label = "Replication"; + String instanceId = "Replication"; String refName = RefNames.REFS_HEADS + "master"; String expectedPayload = @@ -150,38 +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"); - when(config.getString("replication", null, "instanceLabel")).thenReturn(label); - - HttpResult httpResult = new HttpResult(SC_CREATED, Optional.of("result message")); - when(httpClient.execute(any(HttpPost.class), any(), any())).thenReturn(httpResult); - when(httpClientFactory.create(any())).thenReturn(httpClient); - syncRefsFilter = new SyncRefsFilter(replicationConfig); - objectUnderTest = - new FetchRestApiClient( - credentials, httpClientFactory, replicationConfig, syncRefsFilter, pluginName, source); - } + protected abstract void assertAuthentication(HttpRequestBase httpRequest); @Test public void shouldCallFetchEndpoint() @@ -189,26 +152,24 @@ objectUnderTest.callFetch(Project.nameKey("test_repo"), refName, new URIish(api)); - verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any(), any()); + verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any()); 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, source); - objectUnderTest.callFetch(Project.nameKey("test_repo"), refName, new URIish(api)); - verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any(), any()); + verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any()); HttpPost httpPost = httpPostCaptor.getValue(); assertThat(readPayload(httpPost)).isEqualTo(expectedPayload); @@ -223,11 +184,18 @@ syncRefsFilter = new SyncRefsFilter(replicationConfig); objectUnderTest = new FetchRestApiClient( - credentials, httpClientFactory, replicationConfig, syncRefsFilter, pluginName, source); + credentials, + httpClientFactory, + replicationConfig, + syncRefsFilter, + pluginName, + instanceId, + bearerTokenProvider, + source); objectUnderTest.callFetch(Project.nameKey("test_repo"), refName, new URIish(api)); - verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any(), any()); + verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any()); HttpPost httpPost = httpPostCaptor.getValue(); assertThat(readPayload(httpPost)).isEqualTo(expectedAsyncPayload); @@ -245,15 +213,22 @@ syncRefsFilter = new SyncRefsFilter(replicationConfig); objectUnderTest = new FetchRestApiClient( - credentials, httpClientFactory, replicationConfig, syncRefsFilter, pluginName, source); + credentials, + httpClientFactory, + replicationConfig, + syncRefsFilter, + pluginName, + instanceId, + bearerTokenProvider, + source); objectUnderTest.callFetch(Project.nameKey("test_repo"), refName, new URIish(api)); - verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any(), any()); + verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any()); HttpPost httpPost = httpPostCaptor.getValue(); assertThat(readPayload(httpPost)).isEqualTo(expectedAsyncPayload); objectUnderTest.callFetch(Project.nameKey("test_repo"), metaRefName, new URIish(api)); - verify(httpClient, times(2)).execute(httpPostCaptor.capture(), any(), any()); + verify(httpClient, times(2)).execute(httpPostCaptor.capture(), any()); httpPost = httpPostCaptor.getValue(); assertThat(readPayload(httpPost)).isEqualTo(expectedMetaRefPayload); } @@ -264,7 +239,7 @@ objectUnderTest.callFetch(Project.nameKey("test_repo"), refName, new URIish(api)); - verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any(), any()); + verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any()); HttpPost httpPost = httpPostCaptor.getValue(); assertThat(readPayload(httpPost)).isEqualTo(expectedPayload); @@ -276,7 +251,7 @@ objectUnderTest.callFetch(Project.nameKey("test_repo"), refName, new URIish(api)); - verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any(), any()); + verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any()); HttpPost httpPost = httpPostCaptor.getValue(); assertThat(httpPost.getLastHeader("Content-Type").getValue()) @@ -294,12 +269,15 @@ createSampleRevisionData(), new URIish(api)); - verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any(), any()); + verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any()); 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 @@ -313,7 +291,7 @@ createSampleRevisionData(), new URIish(api)); - verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any(), any()); + verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any()); HttpPost httpPost = httpPostCaptor.getValue(); assertThat(readPayload(httpPost)).isEqualTo(expectedSendObjectPayload); @@ -325,7 +303,7 @@ objectUnderTest.callFetch(Project.nameKey("test_repo"), refName, new URIish(api)); - verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any(), any()); + verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any()); HttpPost httpPost = httpPostCaptor.getValue(); assertThat(httpPost.getLastHeader("Content-Type").getValue()) @@ -334,7 +312,6 @@ @Test public void shouldThrowExceptionWhenInstanceLabelIsNull() { - when(config.getString("replication", null, "instanceLabel")).thenReturn(null); assertThrows( NullPointerException.class, () -> @@ -344,12 +321,13 @@ replicationConfig, syncRefsFilter, pluginName, + null, + bearerTokenProvider, source)); } @Test public void shouldTrimInstanceLabel() { - when(config.getString("replication", null, "instanceLabel")).thenReturn(" "); assertThrows( NullPointerException.class, () -> @@ -359,12 +337,13 @@ replicationConfig, syncRefsFilter, pluginName, + " ", + bearerTokenProvider, source)); } @Test public void shouldThrowExceptionWhenInstanceLabelIsEmpty() { - when(config.getString("replication", null, "instanceLabel")).thenReturn(""); assertThrows( NullPointerException.class, () -> @@ -374,20 +353,48 @@ replicationConfig, syncRefsFilter, pluginName, + "", + bearerTokenProvider, source)); } @Test + public void shouldUseReplicationLabelWhenProvided() + throws ClientProtocolException, IOException, URISyntaxException { + when(config.getString("replication", null, "instanceLabel")).thenReturn(instanceId); + FetchRestApiClient objectUnderTest = + new FetchRestApiClient( + credentials, + httpClientFactory, + replicationConfig, + syncRefsFilter, + pluginName, + "", + bearerTokenProvider, + source); + objectUnderTest.callFetch(Project.nameKey("test_repo"), refName, new URIish(api)); + + verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any()); + + HttpPost httpPost = httpPostCaptor.getValue(); + assertThat(readPayload(httpPost)).isEqualTo(expectedPayload); + } + + @Test public void shouldCallInitProjectEndpoint() throws IOException, URISyntaxException { objectUnderTest.initProject(Project.nameKey("test_repo"), new URIish(api)); - verify(httpClient, times(1)).execute(httpPutCaptor.capture(), any(), any()); + verify(httpClient, times(1)).execute(httpPutCaptor.capture(), any()); 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 @@ -395,12 +402,16 @@ objectUnderTest.deleteProject(Project.nameKey("test_repo"), new URIish(api)); - verify(httpClient, times(1)).execute(httpDeleteCaptor.capture(), any(), any()); + verify(httpClient, times(1)).execute(httpDeleteCaptor.capture(), any()); 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 @@ -409,7 +420,7 @@ String projectName = "aProject"; objectUnderTest.updateHead(Project.nameKey(projectName), newHead, new URIish(api)); - verify(httpClient, times(1)).execute(httpPutCaptor.capture(), any(), any()); + verify(httpClient, times(1)).execute(httpPutCaptor.capture(), any()); HttpPut httpPut = httpPutCaptor.getValue(); String payload = @@ -418,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"); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java new file mode 100644 index 0000000..c673011 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java
@@ -0,0 +1,150 @@ +// 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.event; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.server.data.RefUpdateAttribute; +import com.google.gerrit.server.events.Event; +import com.google.gerrit.server.events.ProjectCreatedEvent; +import com.google.gerrit.server.events.RefUpdatedEvent; +import com.google.gerrit.server.git.WorkQueue; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.googlesource.gerrit.plugins.replication.pull.FetchOne; +import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input; +import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob; +import com.googlesource.gerrit.plugins.replication.pull.api.ProjectInitializationAction; +import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics; +import java.util.concurrent.ScheduledExecutorService; +import org.eclipse.jgit.lib.ObjectId; +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.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class StreamEventListenerTest { + + private static final String TEST_REF_NAME = "refs/changes/01/1/1"; + private static final String TEST_PROJECT = "test-project"; + private static final String INSTANCE_ID = "node_instance_id"; + private static final String REMOTE_INSTANCE_ID = "remote_node_instance_id"; + + @Mock private ProjectInitializationAction projectInitializationAction; + @Mock private WorkQueue workQueue; + @Mock private ScheduledExecutorService executor; + @Mock private FetchJob fetchJob; + @Mock private FetchJob.Factory fetchJobFactory; + @Captor ArgumentCaptor<Input> inputCaptor; + @Mock private PullReplicationApiRequestMetrics metrics; + + private StreamEventListener objectUnderTest; + + @Before + public void setup() { + when(workQueue.getDefaultQueue()).thenReturn(executor); + when(fetchJobFactory.create(eq(Project.nameKey(TEST_PROJECT)), any(), any())) + .thenReturn(fetchJob); + objectUnderTest = + new StreamEventListener( + INSTANCE_ID, projectInitializationAction, workQueue, fetchJobFactory, () -> metrics); + } + + @Test + public void shouldSkipEventsGeneratedByTheSameInstance() { + Event event = new RefUpdatedEvent(); + event.instanceId = INSTANCE_ID; + objectUnderTest.onEvent(event); + + verify(executor, never()).submit(any(Runnable.class)); + } + + @Test + public void shouldSkipFetchForProjectDeleteEvent() { + RefUpdatedEvent event = new RefUpdatedEvent(); + RefUpdateAttribute refUpdate = new RefUpdateAttribute(); + refUpdate.refName = RefNames.REFS_CONFIG; + refUpdate.newRev = ObjectId.zeroId().getName(); + refUpdate.project = TEST_PROJECT; + + event.instanceId = REMOTE_INSTANCE_ID; + event.refUpdate = () -> refUpdate; + + objectUnderTest.onEvent(event); + + verify(executor, never()).submit(any(Runnable.class)); + } + + @Test + public void shouldScheduleFetchJobForRefUpdateEvent() { + RefUpdatedEvent event = new RefUpdatedEvent(); + RefUpdateAttribute refUpdate = new RefUpdateAttribute(); + refUpdate.refName = TEST_REF_NAME; + refUpdate.project = TEST_PROJECT; + + event.instanceId = REMOTE_INSTANCE_ID; + event.refUpdate = () -> refUpdate; + + objectUnderTest.onEvent(event); + + verify(fetchJobFactory).create(eq(Project.nameKey(TEST_PROJECT)), inputCaptor.capture(), any()); + + Input input = inputCaptor.getValue(); + assertThat(input.label).isEqualTo(REMOTE_INSTANCE_ID); + assertThat(input.refName).isEqualTo(TEST_REF_NAME); + + verify(executor).submit(any(FetchJob.class)); + } + + @Test + public void shouldCreateProjectForProjectCreatedEvent() + throws AuthException, PermissionBackendException { + ProjectCreatedEvent event = new ProjectCreatedEvent(); + event.instanceId = REMOTE_INSTANCE_ID; + event.projectName = TEST_PROJECT; + + objectUnderTest.onEvent(event); + + verify(projectInitializationAction).initProject(String.format("%s.git", TEST_PROJECT)); + } + + @Test + public void shouldScheduleAllRefsFetchForProjectCreatedEvent() { + ProjectCreatedEvent event = new ProjectCreatedEvent(); + event.instanceId = REMOTE_INSTANCE_ID; + event.projectName = TEST_PROJECT; + + objectUnderTest.onEvent(event); + + verify(fetchJobFactory).create(eq(Project.nameKey(TEST_PROJECT)), inputCaptor.capture(), any()); + + Input input = inputCaptor.getValue(); + assertThat(input.label).isEqualTo(REMOTE_INSTANCE_ID); + assertThat(input.refName).isEqualTo(FetchOne.ALL_REFS); + + verify(executor).submit(any(FetchJob.class)); + } +}
diff --git a/src/test/java/org/eclipse/jgit/transport/TransportHttpWithBearerTokenTest.java b/src/test/java/org/eclipse/jgit/transport/TransportHttpWithBearerTokenTest.java new file mode 100644 index 0000000..5bf1047 --- /dev/null +++ b/src/test/java/org/eclipse/jgit/transport/TransportHttpWithBearerTokenTest.java
@@ -0,0 +1,52 @@ +// 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 org.eclipse.jgit.transport; + +import static com.google.common.truth.Truth.assertThat; + +import java.net.URISyntaxException; +import org.junit.Test; + +public class TransportHttpWithBearerTokenTest { + + @Test + public void cannotHandleURIWhenSchemaIsNeitherHttpNorHttps() throws URISyntaxException { + URIish uriUnderTest = new URIish("some-uri").setScheme(null); + boolean result = TransportHttpWithBearerToken.canHandle(uriUnderTest); + assertThat(result).isFalse(); + } + + @Test + public void cannotHandleURIWhenHostIsNotPresent() throws URISyntaxException { + URIish uriUnderTest = new URIish("some-uri").setScheme("http").setHost(null); + boolean result = TransportHttpWithBearerToken.canHandle(uriUnderTest); + assertThat(result).isFalse(); + } + + @Test + public void cannotHandleURIWhenPathIsNotPresent() throws URISyntaxException { + URIish uriUnderTest = + new URIish("some-uri").setScheme("http").setHost("some-host").setPath(null); + boolean result = TransportHttpWithBearerToken.canHandle(uriUnderTest); + assertThat(result).isFalse(); + } + + @Test + public void canHandleURIWhenIsWellFormed() throws URISyntaxException { + URIish uriUnderTest = new URIish("http://some-host/some-path"); + boolean result = TransportHttpWithBearerToken.canHandle(uriUnderTest); + assertThat(result).isTrue(); + } +}
diff --git a/src/test/java/org/eclipse/jgit/transport/TransportProviderTest.java b/src/test/java/org/eclipse/jgit/transport/TransportProviderTest.java new file mode 100644 index 0000000..ea70c40 --- /dev/null +++ b/src/test/java/org/eclipse/jgit/transport/TransportProviderTest.java
@@ -0,0 +1,103 @@ +// 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 org.eclipse.jgit.transport; + +import static com.google.common.truth.Truth.assertThat; +import static org.eclipse.jgit.transport.HttpConfig.EXTRA_HEADER; +import static org.eclipse.jgit.transport.HttpConfig.HTTP; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.googlesource.gerrit.plugins.replication.CredentialsFactory; +import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider; +import com.googlesource.gerrit.plugins.replication.pull.SourceConfiguration; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Optional; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class TransportProviderTest { + @Mock private SourceConfiguration sourceConfig; + @Mock private CredentialsFactory cpFactory; + @Mock private RemoteConfig remoteConfig; + @Mock private BearerTokenProvider bearerTokenProvider; + @Mock private Repository repository; + @Mock private StoredConfig storedConfig; + @Mock private org.eclipse.jgit.transport.TransferConfig transferConfig; + + @Before + public void setup() { + when(sourceConfig.getRemoteConfig()).thenReturn(remoteConfig); + when(repository.getConfig()).thenReturn(storedConfig); + String[] emptyHeaders = {}; + when(storedConfig.getStringList(HTTP, null, EXTRA_HEADER)).thenReturn(emptyHeaders); + when(storedConfig.get(TransferConfig.KEY)).thenReturn(transferConfig); + } + + private void verifyConstructor() { + verify(sourceConfig).getRemoteConfig(); + verify(remoteConfig).getName(); + verify(bearerTokenProvider).get(); + } + + @Test + public void shouldProvideTransportHttpWithBearerToken() throws URISyntaxException, IOException { + when(bearerTokenProvider.get()).thenReturn(Optional.of("some-bearer-token")); + + TransportProvider transportProvider = + new TransportProvider(sourceConfig, cpFactory, bearerTokenProvider); + verifyConstructor(); + + URIish urIish = new URIish("http://some-host/some-path"); + Transport transport = transportProvider.open(repository, urIish); + assertThat(transport).isInstanceOf(TransportHttpWithBearerToken.class); + } + + @Test + public void shouldProvideNativeTransportWhenNoBearerTokenProvided() + throws URISyntaxException, IOException { + + when(bearerTokenProvider.get()).thenReturn(Optional.empty()); + + TransportProvider transportProvider = + new TransportProvider(sourceConfig, cpFactory, bearerTokenProvider); + verifyConstructor(); + + URIish urIish = new URIish("ssh://some-host/some-path"); + Transport transport = transportProvider.open(repository, urIish); + assertThat(transport).isNotInstanceOf(TransportHttpWithBearerToken.class); + } + + @Test + public void shouldProvideNativeTransportWhenNoHttpSchemeProvided() + throws URISyntaxException, IOException { + when(bearerTokenProvider.get()).thenReturn(Optional.of("some-bearer-token")); + + TransportProvider transportProvider = + new TransportProvider(sourceConfig, cpFactory, bearerTokenProvider); + verifyConstructor(); + + URIish urIish = new URIish("ssh://some-host/some-path"); + Transport transport = transportProvider.open(repository, urIish); + assertThat(transport).isNotInstanceOf(TransportHttpWithBearerToken.class); + } +}