| // Copyright (C) 2015 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.google.gerrit.acceptance; |
| |
| import static com.google.gerrit.server.git.receive.LazyPostReceiveHookChain.affectsSize; |
| import static com.google.gerrit.server.project.ProjectCache.illegalState; |
| import static com.google.gerrit.server.quota.QuotaGroupDefinitions.REPOSITORY_SIZE_GROUP; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Lists; |
| import com.google.gerrit.acceptance.InProcessProtocol.Context; |
| import com.google.gerrit.common.data.Capable; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.extensions.registration.DynamicSet; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.server.AccessPath; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.RemotePeer; |
| import com.google.gerrit.server.RequestCleanup; |
| import com.google.gerrit.server.config.GerritRequestModule; |
| import com.google.gerrit.server.git.PermissionAwareRepositoryManager; |
| import com.google.gerrit.server.git.ReceivePackInitializer; |
| import com.google.gerrit.server.git.TransferConfig; |
| import com.google.gerrit.server.git.UploadPackInitializer; |
| import com.google.gerrit.server.git.UsersSelfAdvertiseRefsHook; |
| import com.google.gerrit.server.git.receive.AsyncReceiveCommits; |
| import com.google.gerrit.server.git.validators.UploadValidators; |
| import com.google.gerrit.server.permissions.PermissionBackend; |
| import com.google.gerrit.server.permissions.PermissionBackendException; |
| import com.google.gerrit.server.permissions.ProjectPermission; |
| import com.google.gerrit.server.plugincontext.PluginSetContext; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.gerrit.server.quota.QuotaBackend; |
| import com.google.gerrit.server.quota.QuotaException; |
| import com.google.gerrit.server.quota.QuotaResponse; |
| import com.google.gerrit.server.util.RequestContext; |
| import com.google.gerrit.server.util.RequestScopePropagator; |
| import com.google.gerrit.server.util.ThreadLocalRequestContext; |
| import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator; |
| import com.google.inject.AbstractModule; |
| import com.google.inject.Inject; |
| import com.google.inject.Key; |
| import com.google.inject.Module; |
| import com.google.inject.OutOfScopeException; |
| import com.google.inject.Provider; |
| import com.google.inject.Provides; |
| import com.google.inject.Scope; |
| import com.google.inject.servlet.RequestScoped; |
| import java.io.IOException; |
| import java.net.SocketAddress; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.transport.PostReceiveHook; |
| import org.eclipse.jgit.transport.PostReceiveHookChain; |
| import org.eclipse.jgit.transport.PreUploadHook; |
| import org.eclipse.jgit.transport.PreUploadHookChain; |
| import org.eclipse.jgit.transport.ReceivePack; |
| import org.eclipse.jgit.transport.TestProtocol; |
| import org.eclipse.jgit.transport.UploadPack; |
| import org.eclipse.jgit.transport.resolver.ReceivePackFactory; |
| import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; |
| import org.eclipse.jgit.transport.resolver.UploadPackFactory; |
| |
| class InProcessProtocol extends TestProtocol<Context> { |
| static Module module() { |
| return new AbstractModule() { |
| @Override |
| public void configure() { |
| install(new GerritRequestModule()); |
| bind(RequestScopePropagator.class).to(Propagator.class); |
| bindScope(RequestScoped.class, InProcessProtocol.REQUEST); |
| } |
| |
| @Provides |
| @RemotePeer |
| SocketAddress getSocketAddress() { |
| throw new OutOfScopeException("No remote peer in acceptance tests"); |
| } |
| }; |
| } |
| |
| private static final Scope REQUEST = |
| new Scope() { |
| @Override |
| public <T> Provider<T> scope(Key<T> key, Provider<T> creator) { |
| return new Provider<>() { |
| @Override |
| public T get() { |
| Context ctx = current.get(); |
| if (ctx == null) { |
| throw new OutOfScopeException("Not in TestProtocol scope"); |
| } |
| return ctx.get(key, creator); |
| } |
| |
| @Override |
| public String toString() { |
| return String.format("%s[%s]", creator, REQUEST); |
| } |
| }; |
| } |
| |
| @Override |
| public String toString() { |
| return "InProcessProtocol.REQUEST"; |
| } |
| }; |
| |
| private static class Propagator extends ThreadLocalRequestScopePropagator<Context> { |
| @Inject |
| Propagator(ThreadLocalRequestContext local) { |
| super(REQUEST, current, local); |
| } |
| |
| @Override |
| protected Context continuingContext(Context ctx) { |
| return ctx.newContinuingContext(); |
| } |
| } |
| |
| private static final ThreadLocal<Context> current = new ThreadLocal<>(); |
| |
| // TODO(dborowitz): Merge this with AcceptanceTestRequestScope. |
| /** |
| * Multi-purpose session/context object. |
| * |
| * <p>Confusingly, Gerrit has two ideas of what a "context" object is: one for Guice {@link |
| * RequestScoped}, and one for its own simplified version of request scoping using {@link |
| * ThreadLocalRequestContext}. This class provides both, in essence just delegating the {@code |
| * ThreadLocalRequestContext} scoping to the Guice scoping mechanism. |
| * |
| * <p>It is also used as the session type for {@code UploadPackFactory} and {@code |
| * ReceivePackFactory}, since, after all, it encapsulates all the information about a single |
| * request. |
| */ |
| static class Context implements RequestContext { |
| private static final Key<RequestCleanup> RC_KEY = Key.get(RequestCleanup.class); |
| private static final Key<CurrentUser> USER_KEY = Key.get(CurrentUser.class); |
| |
| private final IdentifiedUser.GenericFactory userFactory; |
| private final Account.Id accountId; |
| private final Project.NameKey project; |
| private final RequestCleanup cleanup; |
| private final Map<Key<?>, Object> map; |
| |
| Context( |
| IdentifiedUser.GenericFactory userFactory, Account.Id accountId, Project.NameKey project) { |
| this.userFactory = userFactory; |
| this.accountId = accountId; |
| this.project = project; |
| map = new HashMap<>(); |
| cleanup = new RequestCleanup(); |
| map.put(RC_KEY, cleanup); |
| |
| IdentifiedUser user = userFactory.create(accountId); |
| user.setAccessPath(AccessPath.GIT); |
| map.put(USER_KEY, user); |
| } |
| |
| private Context newContinuingContext() { |
| return new Context(userFactory, accountId, project); |
| } |
| |
| @Override |
| public CurrentUser getUser() { |
| return get(USER_KEY, null); |
| } |
| |
| private synchronized <T> T get(Key<T> key, Provider<T> creator) { |
| @SuppressWarnings("unchecked") |
| T t = (T) map.get(key); |
| if (t == null) { |
| t = creator.get(); |
| map.put(key, t); |
| } |
| return t; |
| } |
| } |
| |
| private static class Upload implements UploadPackFactory<Context> { |
| private final TransferConfig transferConfig; |
| private final PluginSetContext<UploadPackInitializer> uploadPackInitializers; |
| private final DynamicSet<PreUploadHook> preUploadHooks; |
| private final UploadValidators.Factory uploadValidatorsFactory; |
| private final ThreadLocalRequestContext threadContext; |
| private final ProjectCache projectCache; |
| private final PermissionBackend permissionBackend; |
| private final UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook; |
| |
| @Inject |
| Upload( |
| TransferConfig transferConfig, |
| PluginSetContext<UploadPackInitializer> uploadPackInitializers, |
| DynamicSet<PreUploadHook> preUploadHooks, |
| UploadValidators.Factory uploadValidatorsFactory, |
| ThreadLocalRequestContext threadContext, |
| ProjectCache projectCache, |
| PermissionBackend permissionBackend, |
| UsersSelfAdvertiseRefsHook usersSelfAdvertiseRefsHook) { |
| this.transferConfig = transferConfig; |
| this.uploadPackInitializers = uploadPackInitializers; |
| this.preUploadHooks = preUploadHooks; |
| this.uploadValidatorsFactory = uploadValidatorsFactory; |
| this.threadContext = threadContext; |
| this.projectCache = projectCache; |
| this.permissionBackend = permissionBackend; |
| this.usersSelfAdvertiseRefsHook = usersSelfAdvertiseRefsHook; |
| } |
| |
| @Override |
| public UploadPack create(Context req, Repository repo) throws ServiceNotAuthorizedException { |
| // Set the request context, but don't bother unsetting, since we don't |
| // have an easy way to run code when this instance is done being used. |
| // Each operation is run in its own thread, so we don't need to recover |
| // its original context anyway. |
| @SuppressWarnings("unused") |
| var unused = threadContext.setContext(req); |
| |
| current.set(req); |
| |
| PermissionBackend.ForProject perm = permissionBackend.currentUser().project(req.project); |
| try { |
| if (!perm.test(ProjectPermission.RUN_UPLOAD_PACK)) { |
| throw new ServiceNotAuthorizedException("upload pack not permitted"); |
| } |
| } catch (PermissionBackendException e) { |
| throw new RuntimeException(e); |
| } |
| |
| ProjectState projectState = |
| projectCache.get(req.project).orElseThrow(illegalState(req.project)); |
| Repository permissionAwareRepository = PermissionAwareRepositoryManager.wrap(repo, perm); |
| UploadPack up = new UploadPack(permissionAwareRepository); |
| up.setPackConfig(transferConfig.getPackConfig()); |
| up.setTimeout(transferConfig.getTimeout()); |
| if (projectState.isAllUsers()) { |
| up.setAdvertiseRefsHook(usersSelfAdvertiseRefsHook); |
| } |
| List<PreUploadHook> hooks = Lists.newArrayList(preUploadHooks); |
| hooks.add( |
| uploadValidatorsFactory.create( |
| projectState.getProject(), permissionAwareRepository, "localhost-test")); |
| up.setPreUploadHook(PreUploadHookChain.newChain(hooks)); |
| uploadPackInitializers.runEach(initializer -> initializer.init(req.project, up)); |
| return up; |
| } |
| } |
| |
| private static class Receive implements ReceivePackFactory<Context> { |
| private final Provider<CurrentUser> userProvider; |
| private final ProjectCache projectCache; |
| private final AsyncReceiveCommits.Factory factory; |
| private final TransferConfig config; |
| private final PluginSetContext<ReceivePackInitializer> receivePackInitializers; |
| private final DynamicSet<PostReceiveHook> postReceiveHooks; |
| private final ThreadLocalRequestContext threadContext; |
| private final PermissionBackend permissionBackend; |
| private final QuotaBackend quotaBackend; |
| |
| @Inject |
| Receive( |
| Provider<CurrentUser> userProvider, |
| ProjectCache projectCache, |
| AsyncReceiveCommits.Factory factory, |
| TransferConfig config, |
| PluginSetContext<ReceivePackInitializer> receivePackInitializers, |
| DynamicSet<PostReceiveHook> postReceiveHooks, |
| ThreadLocalRequestContext threadContext, |
| PermissionBackend permissionBackend, |
| QuotaBackend quotaBackend) { |
| this.userProvider = userProvider; |
| this.projectCache = projectCache; |
| this.factory = factory; |
| this.config = config; |
| this.receivePackInitializers = receivePackInitializers; |
| this.postReceiveHooks = postReceiveHooks; |
| this.threadContext = threadContext; |
| this.permissionBackend = permissionBackend; |
| this.quotaBackend = quotaBackend; |
| } |
| |
| @Override |
| public ReceivePack create(Context req, Repository db) throws ServiceNotAuthorizedException { |
| // Set the request context, but don't bother unsetting, since we don't |
| // have an easy way to run code when this instance is done being used. |
| // Each operation is run in its own thread, so we don't need to recover |
| // its original context anyway. |
| @SuppressWarnings("unused") |
| var unused = threadContext.setContext(req); |
| |
| current.set(req); |
| try { |
| permissionBackend |
| .currentUser() |
| .project(req.project) |
| .check(ProjectPermission.RUN_RECEIVE_PACK); |
| } catch (AuthException e) { |
| throw new ServiceNotAuthorizedException(e.getMessage(), e); |
| } catch (PermissionBackendException e) { |
| throw new RuntimeException(e); |
| } |
| try { |
| IdentifiedUser identifiedUser = userProvider.get().asIdentifiedUser(); |
| ProjectState projectState = |
| projectCache |
| .get(req.project) |
| .orElseThrow( |
| () -> new RuntimeException(String.format("project %s not found", req.project))); |
| |
| AsyncReceiveCommits arc = factory.create(projectState, identifiedUser, db, null); |
| if (arc.canUpload() != Capable.OK) { |
| throw new ServiceNotAuthorizedException(); |
| } |
| |
| ReceivePack rp = arc.getReceivePack(); |
| rp.setRefLogIdent(identifiedUser.newRefLogIdent()); |
| rp.setTimeout(config.getTimeout()); |
| rp.setMaxObjectSizeLimit(config.getMaxObjectSizeLimit()); |
| |
| receivePackInitializers.runEach( |
| initializer -> initializer.init(projectState.getNameKey(), rp)); |
| QuotaResponse.Aggregated availableTokens = |
| quotaBackend |
| .user(identifiedUser) |
| .project(req.project) |
| .availableTokens(REPOSITORY_SIZE_GROUP); |
| availableTokens.throwOnError(); |
| availableTokens.availableTokens().ifPresent(rp::setMaxPackSizeLimit); |
| |
| ImmutableList<PostReceiveHook> hooks = |
| ImmutableList.<PostReceiveHook>builder() |
| .add( |
| (pack, commands) -> { |
| if (affectsSize(pack)) { |
| try { |
| quotaBackend |
| .user(identifiedUser) |
| .project(req.project) |
| .requestTokens(REPOSITORY_SIZE_GROUP, pack.getPackSize()) |
| .throwOnError(); |
| } catch (QuotaException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| }) |
| .addAll(postReceiveHooks) |
| .build(); |
| rp.setPostReceiveHook(PostReceiveHookChain.newChain(hooks)); |
| return rp; |
| } catch (IOException | PermissionBackendException | QuotaException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| |
| @Inject |
| InProcessProtocol(Upload uploadPackFactory, Receive receivePackFactory) { |
| super(uploadPackFactory, receivePackFactory); |
| } |
| } |