Support X-Gerrit-RunAs as a suexec on HTTP

If an HTTP request includes the header:

  X-Gerrit-RunAs: spearce

and is made by a user granted the runAs capability the request
will evaluate as though it was run by user "spearce", and not the
calling user. This roughly matches the behavior of suexec over SSH,
but for HTTP API requests.

The new header takes any specification for an account, accepting
Account.Id, email address, full name, or username.

Change-Id: Ieda546a0db7290e31a8941c1b761c219aa0b63f9
This commit is contained in:
Shawn Pearce
2013-06-10 14:35:39 -07:00
committed by Sasa Zivkov
parent 47126937b5
commit bb027b04d4
8 changed files with 137 additions and 3 deletions

View File

@@ -1266,6 +1266,15 @@ This limit applies not only to the link:cmd-query.html[`gerrit query`]
command, but also to the web UI results pagination size. command, but also to the web UI results pagination size.
[[capability_runAs]]
Run As
~~~~~~
Allow users to impersonate any other user with the X-Gerrit-RunAs
HTTP header on REST API calls. Site administrators do not inherit
this capability; it must be granted explicitly.
[[capability_runGC]] [[capability_runGC]]
Run Garbage Collection Run Garbage Collection
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -70,6 +70,9 @@ public class GlobalCapability {
/** Maximum result limit per executed query. */ /** Maximum result limit per executed query. */
public static final String QUERY_LIMIT = "queryLimit"; public static final String QUERY_LIMIT = "queryLimit";
/** Ability to impersonate another user. */
public static final String RUN_AS = "runAs";
/** Can run the Git garbage collection. */ /** Can run the Git garbage collection. */
public static final String RUN_GC = "runGC"; public static final String RUN_GC = "runGC";
@@ -103,6 +106,7 @@ public class GlobalCapability {
NAMES_ALL.add(KILL_TASK); NAMES_ALL.add(KILL_TASK);
NAMES_ALL.add(PRIORITY); NAMES_ALL.add(PRIORITY);
NAMES_ALL.add(QUERY_LIMIT); NAMES_ALL.add(QUERY_LIMIT);
NAMES_ALL.add(RUN_AS);
NAMES_ALL.add(RUN_GC); NAMES_ALL.add(RUN_GC);
NAMES_ALL.add(START_REPLICATION); NAMES_ALL.add(START_REPLICATION);
NAMES_ALL.add(STREAM_EVENTS); NAMES_ALL.add(STREAM_EVENTS);

View File

@@ -156,6 +156,7 @@ capabilityNames = \
killTask, \ killTask, \
priority, \ priority, \
queryLimit, \ queryLimit, \
runAs, \
runGC, \ runGC, \
startReplication, \ startReplication, \
streamEvents, \ streamEvents, \
@@ -172,6 +173,7 @@ flushCaches = Flush Caches
killTask = Kill Task killTask = Kill Task
priority = Priority priority = Priority
queryLimit = Query Limit queryLimit = Query Limit
runAs = Run As
runGC = Run Garbage Collection runGC = Run Garbage Collection
startReplication = Start Replication startReplication = Start Replication
streamEvents = Stream Events streamEvents = Stream Events

View File

@@ -185,6 +185,7 @@ public final class CacheBasedWebSession implements WebSession {
public void setUserAccountId(Account.Id id) { public void setUserAccountId(Account.Id id) {
key = new Key("id:" + id); key = new Key("id:" + id);
val = new Val(id, 0, false, null, 0, null, null); val = new Val(id, 0, false, null, 0, null, null);
user = null;
} }
@Override @Override

View File

@@ -0,0 +1,113 @@
// Copyright (C) 2013 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.httpd;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import com.google.gerrit.httpd.restapi.RestApiServlet;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.servlet.ServletModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** Allows running a request as another user account. */
@Singleton
class RunAsFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(RunAsFilter.class);
private static final String RUN_AS = "X-Gerrit-RunAs";
static class Module extends ServletModule {
@Override
protected void configureServlets() {
filter("/*").through(RunAsFilter.class);
}
}
private final Provider<WebSession> session;
private final AccountResolver accountResolver;
@Inject
RunAsFilter(Provider<WebSession> session, AccountResolver accountResolver) {
this.session = session;
this.accountResolver = accountResolver;
}
@Override
public void doFilter(ServletRequest request, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String runas = req.getHeader(RUN_AS);
if (runas != null) {
CurrentUser self = session.get().getCurrentUser();
if (!self.getCapabilities().canRunAs()) {
RestApiServlet.replyError(
(HttpServletResponse) res,
SC_FORBIDDEN,
"not permitted to use " + RUN_AS);
return;
}
Account target;
try {
target = accountResolver.find(runas);
} catch (OrmException e) {
log.warn("cannot resolve account for " + RUN_AS, e);
RestApiServlet.replyError(
(HttpServletResponse) res,
SC_INTERNAL_SERVER_ERROR,
"cannot resolve " + RUN_AS);
return;
}
if (target == null) {
RestApiServlet.replyError(
(HttpServletResponse) res,
SC_FORBIDDEN,
"no account matches " + RUN_AS);
return;
}
session.get().setUserAccountId(target.getId());
}
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}

View File

@@ -83,6 +83,7 @@ public class WebModule extends FactoryModule {
if (wantSSL) { if (wantSSL) {
install(new RequireSslFilter.Module()); install(new RequireSslFilter.Module());
} }
install(new RunAsFilter.Module());
switch (authConfig.getAuthType()) { switch (authConfig.getAuthType()) {
case HTTP: case HTTP:

View File

@@ -841,8 +841,8 @@ public class RestApiServlet extends HttpServlet {
} }
} }
static void replyError(HttpServletResponse res, int statusCode, String msg) public static void replyError(HttpServletResponse res, int statusCode,
throws IOException { String msg) throws IOException {
res.setStatus(statusCode); res.setStatus(statusCode);
CacheHeaders.setNotCacheable(res); CacheHeaders.setNotCacheable(res);
replyText(null, res, msg); replyText(null, res, msg);

View File

@@ -130,7 +130,6 @@ public class CapabilityControl {
|| canAdministrateServer(); || canAdministrateServer();
} }
/** @return true if the user can access the database (with gsql). */ /** @return true if the user can access the database (with gsql). */
public boolean canAccessDatabase() { public boolean canAccessDatabase() {
return canPerform(GlobalCapability.ACCESS_DATABASE); return canPerform(GlobalCapability.ACCESS_DATABASE);
@@ -160,6 +159,11 @@ public class CapabilityControl {
|| canAdministrateServer(); || canAdministrateServer();
} }
/** @return true if the user can impersonate another user. */
public boolean canRunAs() {
return canPerform(GlobalCapability.RUN_AS);
}
/** @return which priority queue the user's tasks should be submitted to. */ /** @return which priority queue the user's tasks should be submitted to. */
public QueueProvider.QueueType getQueueType() { public QueueProvider.QueueType getQueueType() {
// If a non-generic group (that is not Anonymous Users or Registered Users) // If a non-generic group (that is not Anonymous Users or Registered Users)