// Copyright (C) 2014 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.googlesource.gerrit.plugins.gitblit.auth; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Collection; import java.util.Collections; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.Keys; import com.gitblit.manager.IRuntimeManager; import com.gitblit.manager.IUserManager; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.httpd.WebSession; import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.GetDiffPreferences; import com.google.gerrit.server.project.ProjectControl; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import com.googlesource.gerrit.plugins.gitblit.app.GitBlitSettings; @Singleton public class GerritGitBlitUserManager implements IUserManager { private static final Logger log = LoggerFactory.getLogger(GerritGitBlitUserManager.class); private final ProjectControl.GenericFactory projectControl; private final Provider<CurrentUser> userProvider; private final Provider<AnonymousUser> anonymousUser; private final GetDiffPreferences getDiffPreferences; @Inject public GerritGitBlitUserManager(final ProjectControl.GenericFactory projectControl, final GitBlitSettings settings, final DynamicItem<WebSession> gerritSession, final Provider<AnonymousUser> anonymousUser, final GetDiffPreferences getDiffPreferences) { this.projectControl = projectControl; this.userProvider = new Provider<CurrentUser>() { @Override public CurrentUser get() { return gerritSession.get().getUser(); } }; this.anonymousUser = anonymousUser; this.getDiffPreferences = getDiffPreferences; if (!settings.getBoolean(Keys.web.authenticateViewPages, false) && !fixAnonymousUser()) { settings.saveSettings(ImmutableMap.of(Keys.web.authenticateViewPages, Boolean.TRUE.toString())); } } @Override public IUserManager start() { return this; } @Override public IUserManager stop() { return this; } @Override public void setup(IRuntimeManager runtimeManager) { } @Override public UserModel getUserModel(String username) { if (username == null || GerritGitBlitUserModel.ANONYMOUS_USER.equals(username)) { return new GerritGitBlitUserModel(projectControl, anonymousUser, getDiffPreferences); } return new GerritGitBlitUserModel(username, projectControl, userProvider, getDiffPreferences); } /** * GitBlit assumes all users (or user accounts) have a username (account name or login name). Gerrit allows users (accounts) to not have a * username, for instance if the account is created or logged in via Google OAuth. I such cases, we have to fake a username for GitBlit. * * @return a GitBlit {@link UserModel} for an unnamed Gerrit account. */ public UserModel getUnnamedGerritUser() { CurrentUser user = userProvider.get(); if (!user.isIdentifiedUser()) { log.warn("\"Logged-in\" user according to session is anonymous."); return new GerritGitBlitUserModel(projectControl, anonymousUser, getDiffPreferences); } IdentifiedUser loggedInUser = (IdentifiedUser) user; // We know that this user has no username. Synthesize one for GitBlit. String fakeUserName = loggedInUser.getAccount().getPreferredEmail(); if (Strings.isNullOrEmpty(fakeUserName)) { fakeUserName = loggedInUser.getAccount().getFullName(); if (Strings.isNullOrEmpty(fakeUserName)) { fakeUserName = "external" + loggedInUser.getAccountId().toString(); } } return new GerritGitBlitUserModel(fakeUserName, projectControl, userProvider, getDiffPreferences); } @Override public String getCookie(UserModel model) { return model.cookie; } @Override public boolean updateUserModel(UserModel model) { return false; } @Override public boolean updateUserModel(String username, UserModel model) { return false; } @Override public boolean deleteUserModel(UserModel model) { return false; } @Override public boolean deleteUser(String username) { return false; } @Override public List<String> getAllUsernames() { return Collections.emptyList(); } @Override public List<UserModel> getAllUsers() { return Collections.emptyList(); } @Override public List<String> getAllTeamNames() { return Collections.emptyList(); } @Override public List<TeamModel> getAllTeams() { return Collections.emptyList(); } @Override public TeamModel getTeamModel(String teamname) { return null; } @Override public boolean updateTeamModel(TeamModel model) { return false; } @Override public boolean updateTeamModel(String teamname, TeamModel model) { return false; } @Override public boolean deleteTeamModel(TeamModel model) { return false; } @Override public boolean deleteTeam(String teamname) { return false; } @Override public List<String> getUsernamesForRepositoryRole(String role) { return Collections.emptyList(); } @Override public boolean renameRepositoryRole(String oldRole, String newRole) { return false; } @Override public boolean deleteRepositoryRole(String role) { return false; } @Override public boolean updateTeamModels(Collection<TeamModel> arg0) { return false; } @Override public boolean updateUserModels(Collection<UserModel> arg0) { return false; } @Override public UserModel getUserModel(char[] cookie) { return null; } @Override public List<String> getTeamNamesForRepositoryRole(String role) { return Collections.emptyList(); } @Override public boolean isInternalAccount(String username) { return false; } /** * Tries to ensure that GitBlit's "anonymous" user obeys the branch visibility defined by Gerrit. * * @return {@code true} if sucessful, {@code false} if unsuccessful */ private boolean fixAnonymousUser() { // XXX Hack alert! // // This replaces the static final field UserModel.ANONYMOUS with a new object from our own user model. // This may or may not work. The problem here is that that object is hard-coded to the UserModel class and will // thus bypass all our Gerrit repository accessibility checks. We do solve this already by overriding a method // in GerritGitBlitRepositoryManager, but that class is lacking an operation to determine branch visibility. // (The separation of concerns inside GitBlit is a bit haphazard; an operation for branch visibility is on the // UserModel, and precisely because of this we need to make sure that the ANONYMOUS object is one of our own // user model class.) If GitBlit used Guice and a named Guice injection for this object instead of a static final // field, we could solve this much more cleanly by binding it in our module to our own object. if (UserModel.ANONYMOUS instanceof GerritGitBlitUserModel) { return true; } try { Field anonymousField = UserModel.class.getDeclaredField("ANONYMOUS"); if (anonymousField != null) { anonymousField.setAccessible(true); // Suppress Java-language accessibility checks Field modifiers = Field.class.getDeclaredField("modifiers"); if (modifiers != null) { modifiers.setAccessible(true); int modifierFlags = anonymousField.getModifiers(); modifiers.set(anonymousField, modifierFlags & ~Modifier.FINAL); // Remove "final" from the "ANONYMOUS" field anonymousField.set(null, new GerritGitBlitUserModel(projectControl, anonymousUser, getDiffPreferences)); modifiers.set(anonymousField, modifierFlags); // Make the field "final" again. modifiers.setAccessible(false); // Re-enable Java-language accessibility checks } anonymousField.setAccessible(false); // Re-enable Java-language accessibility checks } } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { if (UserModel.ANONYMOUS instanceof GerritGitBlitUserModel) { // Was changed, so the exception occurred later log.debug("Reflectively changing the anonymous caused exception after the change", e); } else { if (log.isDebugEnabled()) { log.debug("Reflectively changing the anonymous user failed; disabling anonymous access", e); } else { log.warn("Cannot redefine anonymous user; disabling anonymous access. Error: {}", e.getLocalizedMessage()); } } } if (UserModel.ANONYMOUS instanceof GerritGitBlitUserModel) { log.info("Successfully installed Gerrit anonymous user in GitBlit"); return true; } return false; } }