Merge branch 'stable-3.4' * stable-3.4: Update documentation with the configuration of the bearer token Add bearer token to the git fetch. Associate GitRepositoryManager indirectly through a provider Introduce pull-replication user and internal group Change-Id: I433a8a97eb1d2f06a591135d90af6cfb987375b7
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/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 93bbde0..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
@@ -44,6 +44,7 @@ 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; @@ -73,6 +74,7 @@ @Override protected void configure() { + install(new PullReplicationGroupModule()); bind(BearerTokenProvider.class).in(Scopes.SINGLETON); bind(RevisionReader.class).in(Scopes.SINGLETON); bind(ApplyObject.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 index cbe1ab8..8147149 100644 --- 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
@@ -19,12 +19,12 @@ import com.google.gerrit.httpd.AllRequestFilter; import com.google.gerrit.httpd.WebSession; import com.google.gerrit.server.AccessPath; -import com.google.gerrit.server.PluginUser; import com.google.gerrit.server.util.ManualRequestContext; import com.google.gerrit.server.util.ThreadLocalRequestContext; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.name.Named; +import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationInternalUser; import java.io.IOException; import java.util.Optional; import java.util.regex.Matcher; @@ -46,7 +46,7 @@ private static final String BEARER_TOKEN = "BearerToken"; private final DynamicItem<WebSession> session; private final String pluginName; - private final Provider<PluginUser> pluginUserProvider; + private final PullReplicationInternalUser pluginUser; private final Provider<ThreadLocalRequestContext> threadLocalRequestContext; private final String bearerToken; private final Pattern bearerTokenRegex = Pattern.compile("^Bearer\\s(.+)$"); @@ -55,12 +55,12 @@ BearerAuthenticationFilter( DynamicItem<WebSession> session, @PluginName String pluginName, - Provider<PluginUser> pluginUserProvider, + PullReplicationInternalUser pluginUser, Provider<ThreadLocalRequestContext> threadLocalRequestContext, @Named(BEARER_TOKEN) String bearerToken) { this.session = session; this.pluginName = pluginName; - this.pluginUserProvider = pluginUserProvider; + this.pluginUser = pluginUser; this.threadLocalRequestContext = threadLocalRequestContext; this.bearerToken = bearerToken; } @@ -82,13 +82,13 @@ if (isBasicAuthenticationRequest(requestURI)) { filterChain.doFilter(servletRequest, servletResponse); - } else if (isPullReplicationApiRequest(requestURI)) { + } else if (isPullReplicationApiRequest(requestURI) || isGitUploadPackRequest(httpRequest)) { Optional<String> authorizationHeader = Optional.ofNullable(httpRequest.getHeader("Authorization")); if (isBearerTokenAuthenticated(authorizationHeader, bearerToken)) try (ManualRequestContext ctx = - new ManualRequestContext(pluginUserProvider.get(), threadLocalRequestContext.get())) { + new ManualRequestContext(pluginUser, threadLocalRequestContext.get())) { WebSession ws = session.get(); ws.setAccessPathOk(AccessPath.REST_API, true); filterChain.doFilter(servletRequest, servletResponse); @@ -100,6 +100,13 @@ } } + 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
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 14f2545..d86287a 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.Source; @@ -53,24 +54,24 @@ private final ProjectCache projectCache; private final SourcesCollection sourcesCollection; private final PermissionBackend permissionBackend; - private final LocalDiskRepositoryManager gitManager; + private final GitRepositoryManager gitManager; @Inject public DeleteRefCommand( PullReplicationStateLogger fetchStateLog, ProjectCache projectCache, - DynamicItem<EventDispatcher> eventDispatcher, SourcesCollection sourcesCollection, ApplyObject applyObject, PermissionBackend permissionBackend, - LocalDiskRepositoryManager gitManager) { + DynamicItem<EventDispatcher> eventDispatcher, + LocalGitRepositoryManagerProvider gitManagerProvider) { this.fetchStateLog = fetchStateLog; this.projectCache = projectCache; this.applyObject = applyObject; this.eventDispatcher = eventDispatcher; this.sourcesCollection = sourcesCollection; 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/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/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 f29e572..d3a2859 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -518,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/PullReplicationIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java index 187af3d..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,61 +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.config.GerritConfig; -import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; 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 @@ -82,48 +58,29 @@ 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 { @@ -425,80 +382,4 @@ assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId()); } } - - private Ref getRef(Repository repo, String branchName) throws IOException { - return repo.getRefDatabase().exactRef(branchName); - } - - 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; - } - } - - private void setReplicationSource( - String remoteName, String replicaSuffix, Optional<String> project) - throws IOException, ConfigInvalidException { - setReplicationSource(remoteName, Arrays.asList(replicaSuffix), project); - } - - private void setReplicationSource( - String remoteName, List<String> replicaSuffixes, Optional<String> project) - throws IOException, ConfigInvalidException { - - List<String> replicaUrls = - replicaSuffixes.stream() - .map(suffix -> gitPath.resolve("${name}" + suffix + ".git").toString()) - .collect(toList()); - 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; - } - } }
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/BearerAuthenticationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java index bbbe66f..824496a 100644 --- 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
@@ -15,16 +15,19 @@ 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.PluginUser; 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; @@ -37,37 +40,36 @@ @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<PluginUser> pluginUserProvider; @Mock private Provider<ThreadLocalRequestContext> threadLocalRequestContextProvider; - @Mock private PluginUser pluginUser; + @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 authenticateWithURI(String uri) throws ServletException, IOException { + 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(pluginUserProvider.get()).thenReturn(pluginUser); when(threadLocalRequestContextProvider.get()).thenReturn(threadLocalRequestContext); when(session.get()).thenReturn(webSession); final BearerAuthenticationFilter filter = new BearerAuthenticationFilter( - session, - pluginName, - pluginUserProvider, - threadLocalRequestContextProvider, - bearerToken); + session, pluginName, pluginUser, threadLocalRequestContextProvider, bearerToken); filter.doFilter(httpServletRequest, httpServletResponse, filterChain); - verify(httpServletRequest).getRequestURI(); + verify(httpServletRequest, atMost(2)).getRequestURI(); + verify(httpServletRequest, atMost(1)).getQueryString(); verify(httpServletRequest).getHeader("Authorization"); - verify(pluginUserProvider).get(); verify(threadLocalRequestContextProvider).get(); verify(session).get(); verify(webSession).setAccessPathOk(AccessPath.REST_API, true); @@ -75,38 +77,45 @@ } @Test - public void shouldAuthenticateWithBearerTokenWhenFetch() throws ServletException, IOException { - authenticateWithURI("any-prefix/pull-replication~fetch"); + public void shouldAuthenticateWhenFetch() throws ServletException, IOException { + authenticateAndFilter("any-prefix/pull-replication~fetch", NO_QUERY_PARAMETERS); } @Test - public void shouldAuthenticateWithBearerTokenWhenApplyObject() - throws ServletException, IOException { - authenticateWithURI("any-prefix/pull-replication~apply-object"); + public void shouldAuthenticateWhenApplyObject() throws ServletException, IOException { + authenticateAndFilter("any-prefix/pull-replication~apply-object", NO_QUERY_PARAMETERS); } @Test - public void shouldAuthenticateWithBearerTokenWhenApplyObjects() - throws ServletException, IOException { - authenticateWithURI("any-prefix/pull-replication~apply-objects"); + public void shouldAuthenticateWhenApplyObjects() throws ServletException, IOException { + authenticateAndFilter("any-prefix/pull-replication~apply-objects", NO_QUERY_PARAMETERS); } @Test - public void shouldAuthenticateWithBearerTokenWhenDeleteProject() - throws ServletException, IOException { - authenticateWithURI("any-prefix/pull-replication~delete-project"); + public void shouldAuthenticateWhenDeleteProject() throws ServletException, IOException { + authenticateAndFilter("any-prefix/pull-replication~delete-project", NO_QUERY_PARAMETERS); } @Test - public void shouldAuthenticateWithBearerTokenWhenUpdateHead() - throws ServletException, IOException { - authenticateWithURI("any-prefix/projects/my-project/HEAD"); + public void shouldAuthenticateWhenUpdateHead() throws ServletException, IOException { + authenticateAndFilter("any-prefix/projects/my-project/HEAD", NO_QUERY_PARAMETERS); } @Test - public void shouldAuthenticateWithBearerTokenWhenInitProject() + 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 { - authenticateWithURI("any-prefix/pull-replication/init-project/my-project.git"); + authenticateAndFilter("any-prefix", GIT_UPLOAD_PACK_QUERY_PARAMETER); } @Test @@ -119,7 +128,7 @@ new BearerAuthenticationFilter( session, pluginName, - pluginUserProvider, + pluginUser, threadLocalRequestContextProvider, "some-bearer-token"); filter.doFilter(httpServletRequest, httpServletResponse, filterChain); @@ -138,7 +147,7 @@ new BearerAuthenticationFilter( session, pluginName, - pluginUserProvider, + pluginUser, threadLocalRequestContextProvider, "some-bearer-token"); filter.doFilter(httpServletRequest, httpServletResponse, filterChain); @@ -156,7 +165,7 @@ new BearerAuthenticationFilter( session, pluginName, - pluginUserProvider, + pluginUser, threadLocalRequestContextProvider, "some-bearer-token"); filter.doFilter(httpServletRequest, httpServletResponse, filterChain); @@ -173,12 +182,12 @@ new BearerAuthenticationFilter( session, pluginName, - pluginUserProvider, + pluginUser, threadLocalRequestContextProvider, "some-bearer-token"); filter.doFilter(httpServletRequest, httpServletResponse, filterChain); - verify(httpServletRequest).getRequestURI(); + verify(httpServletRequest, times(2)).getRequestURI(); verify(filterChain).doFilter(httpServletRequest, httpServletResponse); } @@ -192,7 +201,7 @@ new BearerAuthenticationFilter( session, pluginName, - pluginUserProvider, + pluginUser, threadLocalRequestContextProvider, "some-bearer-token"); filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
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 1574e0c..2adf72f 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.Source; import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection; @@ -100,11 +101,11 @@ new DeleteRefCommand( fetchStateLog, projectCache, - eventDispatcherDataItem, sourceCollection, applyObject, permissionBackend, - gitManager); + eventDispatcherDataItem, + new LocalGitRepositoryManagerProvider(gitManager)); } @Test
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/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); + } +}