Remove AuthMethod and add access token to RestApi

Authentication for API calls will be handled using an access token
in the "Authorization: OAuth access_token" style. Browsers do not
use this when making requests unless they use an XmlHttpRequest.
If the value used as the access_token is not available cross-site
then the API call cannot be made by hijacking attempts.

Change-Id: I33654bcaa247cb95a57b03d2df112ca95e970185
This commit is contained in:
Shawn O. Pearce
2012-11-12 12:38:30 -08:00
parent e649d848dc
commit bbeb2a96e4
22 changed files with 88 additions and 146 deletions

View File

@@ -23,7 +23,7 @@ import java.util.List;
public class HostPageData {
public Account account;
public AccountDiffPreference accountDiffPref;
public String xsrfToken;
public String accessToken;
public GerritConfig config;
public Theme theme;
public List<String> plugins;

View File

@@ -93,7 +93,7 @@ public class Gerrit implements EntryPoint {
private static HostPageData.Theme myTheme;
private static Account myAccount;
private static AccountDiffPreference myAccountDiffPref;
private static String xsrfToken;
private static String accessToken;
private static MorphingTabPanel menuLeft;
private static LinkMenuBar menuRight;
@@ -239,6 +239,11 @@ public class Gerrit implements EntryPoint {
return myAccount;
}
/** @return access token to prove user identity during REST API calls. */
public static String getAccessToken() {
return accessToken;
}
/** @return the currently signed in users's diff preferences; null if no diff preferences defined for the account */
public static AccountDiffPreference getAccountDiffPreference() {
return myAccountDiffPref;
@@ -333,7 +338,7 @@ public class Gerrit implements EntryPoint {
static void deleteSessionCookie() {
myAccount = null;
myAccountDiffPref = null;
xsrfToken = null;
accessToken = null;
refreshMenuBar();
// If the cookie was HttpOnly, this request to delete it will
@@ -383,7 +388,7 @@ public class Gerrit implements EntryPoint {
myTheme = result.theme;
if (result.account != null) {
myAccount = result.account;
xsrfToken = result.xsrfToken;
accessToken = result.accessToken;
}
if (result.accountDiffPref != null) {
myAccountDiffPref = result.accountDiffPref;
@@ -530,7 +535,7 @@ public class Gerrit implements EntryPoint {
JsonUtil.setDefaultXsrfManager(new XsrfManager() {
@Override
public String getToken(JsonDefTarget proxy) {
return xsrfToken;
return accessToken;
}
@Override

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.client.rpc;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.RpcStatus;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
@@ -161,6 +162,9 @@ public class RestApi {
public <T extends JavaScriptObject> void send(final AsyncCallback<T> cb) {
RequestBuilder req = new RequestBuilder(RequestBuilder.GET, url.toString());
req.setHeader("Accept", JsonConstants.JSON_TYPE);
if (Gerrit.getAccessToken() != null) {
req.setHeader("Authorization", "OAuth " + Gerrit.getAccessToken());
}
req.setCallback(new MyRequestCallback<T>(cb));
try {
RpcStatus.INSTANCE.onRpcStart();

View File

@@ -25,7 +25,6 @@ import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.config.AuthConfig;
@@ -34,6 +33,8 @@ import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.servlet.RequestScoped;
import org.eclipse.jgit.http.server.GitSmartHttpTools;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -65,9 +66,8 @@ public final class CacheBasedWebSession implements WebSession {
private final AuthConfig authConfig;
private final Provider<AnonymousUser> anonymousProvider;
private final IdentifiedUser.RequestFactory identified;
private AccessPath accessPath = AccessPath.WEB_UI;
private AccessPath accessPath;
private Cookie outCookie;
private AuthMethod authMethod;
private Key key;
private Val val;
@@ -85,7 +85,17 @@ public final class CacheBasedWebSession implements WebSession {
this.anonymousProvider = anonymousProvider;
this.identified = identified;
final String cookie = readCookie();
String cookie = request.getHeader("Authorization");
if (cookie != null && cookie.startsWith("OAuth ")) {
cookie = cookie.substring("OAuth ".length());
accessPath = AccessPath.REST_API;
} else if (cookie != null && GitSmartHttpTools.isGitClient(request)) {
accessPath = AccessPath.GIT;
} else {
cookie = readCookie();
accessPath = AccessPath.WEB_BROWSER;
}
if (cookie != null) {
key = new Key(cookie);
val = manager.get(key);
@@ -93,7 +103,6 @@ public final class CacheBasedWebSession implements WebSession {
key = null;
val = null;
}
authMethod = isSignedIn() ? AuthMethod.COOKIE : AuthMethod.NONE;
if (isSignedIn() && val.needsCookieRefresh()) {
// Cookie is more than half old. Send the cookie again to the
@@ -124,14 +133,8 @@ public final class CacheBasedWebSession implements WebSession {
return val != null;
}
public String getToken() {
return isSignedIn() ? val.getXsrfToken() : null;
}
public boolean isTokenValid(final String inputToken) {
return isSignedIn() //
&& val.getXsrfToken() != null //
&& val.getXsrfToken().equals(inputToken);
public String getAccessToken() {
return isSignedIn() ? key.getToken() : null;
}
public AccountExternalId.Key getLastLoginExternalId() {
@@ -145,8 +148,7 @@ public final class CacheBasedWebSession implements WebSession {
return anonymousProvider.get();
}
public void login(final AuthResult res, final AuthMethod meth,
final boolean rememberMe) {
public void login(final AuthResult res, final boolean rememberMe) {
final Account.Id id = res.getAccountId();
final AccountExternalId.Key identity = res.getExternalId();
@@ -155,22 +157,14 @@ public final class CacheBasedWebSession implements WebSession {
}
key = manager.createKey(id);
val = manager.createVal(key, id, rememberMe, identity, null);
val = manager.createVal(key, id, rememberMe, identity);
saveCookie();
authMethod = meth;
}
/** Change the access path from the default of {@link AccessPath#WEB_UI}. */
public void setAccessPath(AccessPath path) {
accessPath = path;
}
/** Set the user account for this current request only. */
public void setUserAccountId(Account.Id id, AuthMethod method) {
public void setUserAccountId(Account.Id id) {
key = new Key("id:" + id);
val = new Val(id, 0, false, null, "", 0);
authMethod = method;
val = new Val(id, 0, false, null, 0);
}
public void logout() {
@@ -217,8 +211,4 @@ public final class CacheBasedWebSession implements WebSession {
private static boolean isSecure(final HttpServletRequest req) {
return req.isSecure() || "https".equals(req.getScheme());
}
public AuthMethod getAuthMethod() {
return authMethod;
}
}

View File

@@ -19,7 +19,6 @@ import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -100,9 +99,7 @@ class ContainerAuthFilter implements Filter {
rsp.sendError(SC_UNAUTHORIZED);
return false;
}
session.get().setUserAccountId(
who.getAccount().getId(),
AuthMethod.PASSWORD);
session.get().setUserAccountId(who.getAccount().getId());
return true;
}
}

View File

@@ -18,7 +18,6 @@ import com.google.common.cache.Cache;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.cache.CacheModule;
@@ -178,12 +177,10 @@ public class GitOverHttpServlet extends GitServlet {
static class UploadFactory implements UploadPackFactory<HttpServletRequest> {
private final TransferConfig config;
private final Provider<WebSession> session;
@Inject
UploadFactory(TransferConfig tc, Provider<WebSession> session) {
UploadFactory(TransferConfig tc) {
this.config = tc;
this.session = session;
}
@Override
@@ -191,7 +188,6 @@ public class GitOverHttpServlet extends GitServlet {
UploadPack up = new UploadPack(repo);
up.setPackConfig(config.getPackConfig());
up.setTimeout(config.getTimeout());
session.get().setAccessPath(AccessPath.GIT);
return up;
}
}
@@ -241,14 +237,11 @@ public class GitOverHttpServlet extends GitServlet {
static class ReceiveFactory implements ReceivePackFactory<HttpServletRequest> {
private final AsyncReceiveCommits.Factory factory;
private final Provider<WebSession> session;
private final TransferConfig config;
@Inject
ReceiveFactory(AsyncReceiveCommits.Factory factory,
Provider<WebSession> session, TransferConfig config) {
ReceiveFactory(AsyncReceiveCommits.Factory factory, TransferConfig config) {
this.factory = factory;
this.session = session;
this.config = config;
}
@@ -269,7 +262,6 @@ public class GitOverHttpServlet extends GitServlet {
rp.setTimeout(config.getTimeout());
rp.setMaxObjectSizeLimit(config.getMaxObjectSizeLimit());
req.setAttribute(ATT_RC, rc);
session.get().setAccessPath(AccessPath.GIT);
return rp;
}
}

View File

@@ -77,7 +77,7 @@ class HttpLogoutServlet extends HttpServlet {
protected void doGet(final HttpServletRequest req,
final HttpServletResponse rsp) throws IOException {
final String sid = webSession.get().getToken();
final String sid = webSession.get().getAccessToken();
final CurrentUser currentUser = webSession.get().getCurrentUser();
final String what = "sign out";
final long when = System.currentTimeMillis();

View File

@@ -22,7 +22,6 @@ import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.config.AuthConfig;
@@ -143,8 +142,7 @@ class ProjectBasicAuthFilter implements Filter {
try {
AuthResult whoAuthResult = accountManager.authenticate(whoAuth);
session.get().setUserAccountId(whoAuthResult.getAccountId(),
AuthMethod.PASSWORD);
session.get().setUserAccountId(whoAuthResult.getAccountId());
return true;
} catch (AccountException e) {
log.warn("Authentication failed for " + username, e);

View File

@@ -22,7 +22,6 @@ import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gwtjsonrpc.server.SignedToken;
@@ -165,9 +164,7 @@ class ProjectDigestFilter implements Filter {
if (expect.equals(response)) {
try {
if (tokens.checkToken(nonce, "") != null) {
session.get().setUserAccountId(
who.getAccount().getId(),
AuthMethod.PASSWORD);
session.get().setUserAccountId(who.getAccount().getId());
return true;
} else {

View File

@@ -20,6 +20,8 @@ import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.CapabilityControl;
@@ -90,8 +92,11 @@ public abstract class RestApiServlet extends HttpServlet {
res.setHeader("Content-Disposition", "attachment");
try {
checkUserSession(req);
checkRequiresCapability();
super.service(req, res);
} catch (InvalidAuthException err) {
sendError(res, SC_FORBIDDEN, err.getMessage());
} catch (RequireCapabilityException err) {
sendError(res, SC_FORBIDDEN, err.getMessage());
} catch (Error err) {
@@ -101,6 +106,18 @@ public abstract class RestApiServlet extends HttpServlet {
}
}
private void checkUserSession(HttpServletRequest req)
throws InvalidAuthException {
CurrentUser user = currentUser.get();
if (user instanceof AnonymousUser) {
if (!"GET".equals(req.getMethod())) {
throw new InvalidAuthException("Authentication required");
}
} else if (user.getAccessPath() != AccessPath.REST_API) {
throw new InvalidAuthException("Invalid authentication method");
}
}
private void checkRequiresCapability() throws RequireCapabilityException {
RequiresCapability rc = getClass().getAnnotation(RequiresCapability.class);
if (rc != null) {
@@ -229,4 +246,11 @@ public abstract class RestApiServlet extends HttpServlet {
super(msg);
}
}
@SuppressWarnings("serial") // Never serialized or thrown out of this class.
private static class InvalidAuthException extends Exception {
public InvalidAuthException(String msg) {
super(msg);
}
}
}

View File

@@ -16,31 +16,22 @@ package com.google.gerrit.httpd;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.account.AuthResult;
public interface WebSession {
public AuthMethod getAuthMethod();
public boolean isSignedIn();
public String getToken();
public boolean isTokenValid(String inputToken);
public String getAccessToken();
public AccountExternalId.Key getLastLoginExternalId();
public CurrentUser getCurrentUser();
public void login(AuthResult res, AuthMethod meth, boolean rememberMe);
/** Change the access path from the default of {@link AccessPath#WEB_UI}. */
public void setAccessPath(AccessPath path);
public void login(AuthResult res, boolean rememberMe);
/** Set the user account for this current request only. */
public void setUserAccountId(Account.Id id, AuthMethod method);
public void setUserAccountId(Account.Id id);
public void logout();
}

View File

@@ -90,13 +90,11 @@ class WebSessionManager {
final Account.Id who = val.getAccountId();
final boolean remember = val.isPersistentCookie();
final AccountExternalId.Key lastLogin = val.getExternalId();
final String xsrfToken = val.getXsrfToken();
return createVal(key, who, remember, lastLogin, xsrfToken);
return createVal(key, who, remember, lastLogin);
}
Val createVal(final Key key, final Account.Id who, final boolean remember,
final AccountExternalId.Key lastLogin, String xsrfToken) {
final AccountExternalId.Key lastLogin) {
// Refresh the cookie every hour or when it is half-expired.
// This reduces the odds that the user session will be kicked
// early but also avoids us needing to refresh the cookie on
@@ -109,17 +107,7 @@ class WebSessionManager {
final long refreshCookieAt = now + refresh;
final long expiresAt = now + sessionMaxAgeMillis;
if (xsrfToken == null) {
// If we don't yet have a token for this session, establish one.
//
final int nonceLen = 20;
final byte[] rnd = new byte[nonceLen];
prng.nextBytes(rnd);
xsrfToken = CookieBase64.encode(rnd);
}
Val val = new Val(who, refreshCookieAt, remember,
lastLogin, xsrfToken, expiresAt);
Val val = new Val(who, refreshCookieAt, remember, lastLogin, expiresAt);
self.put(key.token, val);
return val;
}
@@ -182,18 +170,15 @@ class WebSessionManager {
private transient long refreshCookieAt;
private transient boolean persistentCookie;
private transient AccountExternalId.Key externalId;
private transient String xsrfToken;
private transient long expiresAt;
Val(final Account.Id accountId, final long refreshCookieAt,
final boolean persistentCookie, final AccountExternalId.Key externalId,
final String xsrfToken,
final long expiresAt) {
this.accountId = accountId;
this.refreshCookieAt = refreshCookieAt;
this.persistentCookie = persistentCookie;
this.externalId = externalId;
this.xsrfToken = xsrfToken;
this.expiresAt = expiresAt;
}
@@ -213,10 +198,6 @@ class WebSessionManager {
return persistentCookie;
}
String getXsrfToken() {
return xsrfToken;
}
private void writeObject(final ObjectOutputStream out) throws IOException {
writeVarInt32(out, 1);
writeVarInt32(out, accountId.get());
@@ -232,9 +213,6 @@ class WebSessionManager {
writeString(out, externalId.get());
}
writeVarInt32(out, 5);
writeString(out, xsrfToken);
writeVarInt32(out, 6);
writeFixInt64(out, expiresAt);
@@ -260,7 +238,7 @@ class WebSessionManager {
externalId = new AccountExternalId.Key(readString(in));
continue;
case 5:
xsrfToken = readString(in);
readString(in);
continue;
case 6:
expiresAt = readFixInt64(in);

View File

@@ -24,7 +24,6 @@ import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.gwtorm.server.OrmException;
@@ -114,7 +113,7 @@ public class BecomeAnyAccountLoginServlet extends HttpServlet {
}
if (res != null) {
webSession.get().login(res, AuthMethod.BACKDOOR, false);
webSession.get().login(res, false);
final StringBuilder rdr = new StringBuilder();
rdr.append(req.getContextPath());
if (IS_DEV && req.getParameter("gwt.codesvr") != null) {

View File

@@ -19,7 +19,6 @@ import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.config.AuthConfig;
@@ -136,8 +135,7 @@ class HttpLoginServlet extends HttpServlet {
}
rdr.append(token);
webSession.get().login(arsp, AuthMethod.COOKIE,
true /* persistent cookie */);
webSession.get().login(arsp, true /* persistent cookie */);
rsp.sendRedirect(rdr.toString());
}

View File

@@ -17,7 +17,6 @@ package com.google.gerrit.httpd.auth.container;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.inject.Inject;
@@ -85,7 +84,7 @@ class HttpsClientSslCertAuthFilter implements Filter {
log.error(err, e);
throw new ServletException(err, e);
}
webSession.get().login(arsp, AuthMethod.COOKIE, true);
webSession.get().login(arsp, true);
chain.doFilter(req, rsp);
}

View File

@@ -21,7 +21,6 @@ import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountUserNameException;
import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.auth.AuthenticationUnavailableException;
@@ -87,8 +86,7 @@ class UserPassAuthServiceImpl implements UserPassAuthService {
result.success = true;
result.isNew = res.isNew();
webSession.get().login(res, AuthMethod.PASSWORD,
true /* persistent cookie */);
webSession.get().login(res, true /* persistent cookie */);
callback.onSuccess(result);
}
}

View File

@@ -178,8 +178,8 @@ public class HostPageServlet extends HttpServlet {
json(((IdentifiedUser) user).getAccount(), w);
w.write(";");
w.write(HPD_ID + ".xsrfToken=");
json(session.get().getToken(), w);
w.write(HPD_ID + ".accessToken=");
json(session.get().getAccessToken(), w);
w.write(";");
w.write(HPD_ID + ".accountDiffPref=");

View File

@@ -131,7 +131,7 @@ final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall>
}
Audit note = (Audit) method.getAnnotation(Audit.class);
if (note != null) {
final String sid = call.getWebSession().getToken();
final String sid = call.getWebSession().getAccessToken();
final CurrentUser username = call.getWebSession().getCurrentUser();
final List<Object> args =
extractParams(note, call);
@@ -249,7 +249,7 @@ final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall>
} else {
// The session must exist, and must be using this token.
//
return session.isSignedIn() && session.isTokenValid(keyIn);
return session.isSignedIn() && keyIn.equals(session.getAccessToken());
}
}

View File

@@ -26,7 +26,6 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.UrlEncoded;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.ConfigUtil;
@@ -417,7 +416,7 @@ class OpenIdServiceImpl implements OpenIdService {
lastId.setMaxAge(0);
}
rsp.addCookie(lastId);
webSession.get().login(arsp, AuthMethod.COOKIE, remember);
webSession.get().login(arsp, remember);
if (arsp.isNew() && claimedIdentifier != null) {
final com.google.gerrit.server.account.AuthRequest linkReq =
new com.google.gerrit.server.account.AuthRequest(
@@ -431,7 +430,7 @@ class OpenIdServiceImpl implements OpenIdService {
case LINK_IDENTIY: {
arsp = accountManager.link(identifiedUser.get().getAccountId(), areq);
webSession.get().login(arsp, AuthMethod.COOKIE, remember);
webSession.get().login(arsp, remember);
callback(false, req, rsp);
break;
}

View File

@@ -19,8 +19,11 @@ public enum AccessPath {
/** An unknown access path, probably should not be special. */
UNKNOWN,
/** Access through the web UI. */
WEB_UI,
/** Access through the REST API. */
REST_API,
/** Access by a web cookie. This path is not protected like REST_API. */
WEB_BROWSER,
/** Access through an SSH command that is not invoked by Git. */
SSH_COMMAND,

View File

@@ -1,30 +0,0 @@
// Copyright (C) 2012 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.server.account;
/** Method by which a user has authenticated for a given request. */
public enum AuthMethod {
/** The user is not authenticated */
NONE,
/** The user is authenticated via a cookie. */
COOKIE,
/** The user authenticated with a password for this request. */
PASSWORD,
/** The user has used a credentialess development feature to login. */
BACKDOOR;
}

View File

@@ -218,7 +218,7 @@ public class RefControl {
}
boolean owner;
switch (getCurrentUser().getAccessPath()) {
case WEB_UI:
case REST_API:
owner = isOwner();
break;
@@ -285,7 +285,7 @@ public class RefControl {
}
switch (getCurrentUser().getAccessPath()) {
case WEB_UI:
case REST_API:
return isOwner() || canPushWithForce();
case GIT: