Disable administrative permissions during X-Gerrit-RunAs
When executing an action on behalf of an administrator, disable the
administrateServer capability during the request. This may limit
the damage a compromised role account could cause by avoiding any
permissions that are not explicitly granted.
Change-Id: I263e1d8e1a645617842f11b7712f79f5c009c6ca
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index b016087..fab71dd 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1271,10 +1271,21 @@
Run As
~~~~~~
-Allow users to impersonate any other user with the X-Gerrit-RunAs HTTP
-header on REST API calls or the link:cmd-suexec.html[suexec] SSH
-command. Site administrators do not inherit this capability; it must
-be granted explicitly.
+Allow users to impersonate any other user with the `X-Gerrit-RunAs`
+HTTP header on REST API calls, or the link:cmd-suexec.html[suexec]
+SSH command.
+
+When impersonating an administrator the Administrate Server capability
+is not honored. This security feature tries to prevent a role with
+Run As capability from modifying the access controls in All-Projects,
+however modification may still be possible if the impersonated user
+has permission to push or submit changes on `refs/meta/config`. Run
+As also blocks using most capabilities including Create User, Run
+Garbage Collection, etc., unless the capability is also explicitly
+granted to a group the administrator is a member of.
+
+Administrators do not automatically inherit this capability; it must
+be explicitly granted.
[[capability_runGC]]
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 80144c4..9968e2a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -185,7 +185,7 @@
public void setUserAccountId(Account.Id id) {
key = new Key("id:" + id);
val = new Val(id, 0, false, null, 0, null, null);
- user = null;
+ user = identified.runAs(id, user);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index 86a6ef8..ae8c6bf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -51,6 +51,20 @@
}
/**
+ * Identity of the authenticated user.
+ * <p>
+ * In the normal case where a user authenticates as themselves
+ * {@code getRealUser() == this}.
+ * <p>
+ * If {@code X-Gerrit-RunAs} or {@code suexec} was used this method returns
+ * the identity of the account that has permission to act on behalf of this
+ * user.
+ */
+ public CurrentUser getRealUser() {
+ return this;
+ }
+
+ /**
* Get the set of groups the user is currently a member of.
* <p>
* The returned set may be a subset of the user's actual groups; if the user's
@@ -76,11 +90,9 @@
/** Capabilities available to this user account. */
public CapabilityControl getCapabilities() {
- CapabilityControl ctl = capabilities;
- if (ctl == null) {
- ctl = capabilityControlFactory.create(this);
- capabilities = ctl;
+ if (capabilities == null) {
+ capabilities = capabilityControlFactory.create(this);
}
- return ctl;
+ return capabilities;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index 3826293..8e61c9a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -97,13 +97,20 @@
public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
return new IdentifiedUser(capabilityControlFactory,
authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
- groupBackend, null, db, id);
+ groupBackend, null, db, id, null);
}
public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
return new IdentifiedUser(capabilityControlFactory,
authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
- groupBackend, Providers.of(remotePeer), null, id);
+ groupBackend, Providers.of(remotePeer), null, id, null);
+ }
+
+ public CurrentUser runAs(SocketAddress remotePeer, Account.Id id,
+ @Nullable CurrentUser caller) {
+ return new IdentifiedUser(capabilityControlFactory,
+ authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
+ groupBackend, Providers.of(remotePeer), null, id, caller);
}
}
@@ -152,7 +159,13 @@
public IdentifiedUser create(Account.Id id) {
return new IdentifiedUser(capabilityControlFactory,
authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
- groupBackend, remotePeerProvider, dbProvider, id);
+ groupBackend, remotePeerProvider, dbProvider, id, null);
+ }
+
+ public IdentifiedUser runAs(Account.Id id, CurrentUser caller) {
+ return new IdentifiedUser(capabilityControlFactory,
+ authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
+ groupBackend, remotePeerProvider, dbProvider, id, caller);
}
}
@@ -183,6 +196,7 @@
private GroupMembership effectiveGroups;
private Set<Change.Id> starredChanges;
private Collection<AccountProjectWatch> notificationFilters;
+ private CurrentUser realUser;
private IdentifiedUser(
CapabilityControl.Factory capabilityControlFactory,
@@ -192,7 +206,9 @@
final Realm realm, final AccountCache accountCache,
final GroupBackend groupBackend,
@Nullable final Provider<SocketAddress> remotePeerProvider,
- @Nullable final Provider<ReviewDb> dbProvider, final Account.Id id) {
+ @Nullable final Provider<ReviewDb> dbProvider,
+ final Account.Id id,
+ @Nullable CurrentUser realUser) {
super(capabilityControlFactory);
this.canonicalUrl = canonicalUrl;
this.accountCache = accountCache;
@@ -202,6 +218,12 @@
this.remotePeerProvider = remotePeerProvider;
this.dbProvider = dbProvider;
this.accountId = id;
+ this.realUser = realUser != null ? realUser : this;
+ }
+
+ @Override
+ public CurrentUser getRealUser() {
+ return realUser;
}
// TODO(cranger): maybe get the state through the accountCache instead.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 4c5bd97..193ea04 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -65,8 +65,12 @@
/** @return true if the user can administer this server. */
public boolean canAdministrateServer() {
if (canAdministrateServer == null) {
- canAdministrateServer = user instanceof PeerDaemonUser
- || matchAny(capabilities.administrateServer, ALLOWED_RULE);
+ if (user.getRealUser() != user) {
+ canAdministrateServer = false;
+ } else {
+ canAdministrateServer = user instanceof PeerDaemonUser
+ || matchAny(capabilities.administrateServer, ALLOWED_RULE);
+ }
}
return canAdministrateServer;
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index 54bb84c..298a099 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -126,8 +126,12 @@
} else {
peer = peerAddress;
}
+ CurrentUser self = caller.get();
+ if (self instanceof PeerDaemonUser) {
+ self = null;
+ }
return new SshSession(session.get(), peer,
- userFactory.create(peer, accountId));
+ userFactory.runAs(peer, accountId, self));
}
private static String join(List<String> args) {