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:
committed by
Sasa Zivkov
parent
47126937b5
commit
bb027b04d4
@@ -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
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user