| // 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. | |
| 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. | |
| 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); | |
| } | |
| } |