diff options
author | mschaefers <mschaefers@scoop-gmbh.de> | 2012-11-30 10:29:51 +0100 |
---|---|---|
committer | mschaefers <mschaefers@scoop-gmbh.de> | 2012-11-30 10:29:51 +0100 |
commit | e2013b91626a4c693053e9859add68ff371f2298 (patch) | |
tree | 23b2fc493fff6a8b6549888e789b256e53bb3723 /src | |
parent | 27ae9095639bb228a1b7ff86a3ebe4264abf05be (diff) | |
download | gitblit-e2013b91626a4c693053e9859add68ff371f2298.tar.gz gitblit-e2013b91626a4c693053e9859add68ff371f2298.zip |
significantly reduce write operations to backing user system.
Diffstat (limited to 'src')
-rw-r--r-- | src/com/gitblit/ConfigUserService.java | 2148 | ||||
-rw-r--r-- | src/com/gitblit/FileUserService.java | 2291 | ||||
-rw-r--r-- | src/com/gitblit/GitblitUserService.java | 607 | ||||
-rw-r--r-- | src/com/gitblit/IUserService.java | 649 | ||||
-rw-r--r-- | src/com/gitblit/LdapUserService.java | 21 |
5 files changed, 2861 insertions, 2855 deletions
diff --git a/src/com/gitblit/ConfigUserService.java b/src/com/gitblit/ConfigUserService.java index 068bbe3a..478d6b31 100644 --- a/src/com/gitblit/ConfigUserService.java +++ b/src/com/gitblit/ConfigUserService.java @@ -1,1074 +1,1074 @@ -/*
- * Copyright 2011 gitblit.com.
- *
- * 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.gitblit;
-
-import java.io.File;
-import java.io.IOException;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import org.eclipse.jgit.lib.StoredConfig;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.gitblit.Constants.AccessPermission;
-import com.gitblit.models.TeamModel;
-import com.gitblit.models.UserModel;
-import com.gitblit.utils.ArrayUtils;
-import com.gitblit.utils.DeepCopier;
-import com.gitblit.utils.StringUtils;
-
-/**
- * ConfigUserService is Gitblit's default user service implementation since
- * version 0.8.0.
- *
- * Users and their repository memberships are stored in a git-style config file
- * which is cached and dynamically reloaded when modified. This file is
- * plain-text, human-readable, and may be edited with a text editor.
- *
- * Additionally, this format allows for expansion of the user model without
- * bringing in the complexity of a database.
- *
- * @author James Moger
- *
- */
-public class ConfigUserService implements IUserService {
-
- private static final String TEAM = "team";
-
- private static final String USER = "user";
-
- private static final String PASSWORD = "password";
-
- private static final String DISPLAYNAME = "displayName";
-
- private static final String EMAILADDRESS = "emailAddress";
-
- private static final String ORGANIZATIONALUNIT = "organizationalUnit";
-
- private static final String ORGANIZATION = "organization";
-
- private static final String LOCALITY = "locality";
-
- private static final String STATEPROVINCE = "stateProvince";
-
- private static final String COUNTRYCODE = "countryCode";
-
- private static final String COOKIE = "cookie";
-
- private static final String REPOSITORY = "repository";
-
- private static final String ROLE = "role";
-
- private static final String MAILINGLIST = "mailingList";
-
- private static final String PRERECEIVE = "preReceiveScript";
-
- private static final String POSTRECEIVE = "postReceiveScript";
-
- private final File realmFile;
-
- private final Logger logger = LoggerFactory.getLogger(ConfigUserService.class);
-
- private final Map<String, UserModel> users = new ConcurrentHashMap<String, UserModel>();
-
- private final Map<String, UserModel> cookies = new ConcurrentHashMap<String, UserModel>();
-
- private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>();
-
- private volatile long lastModified;
-
- private volatile boolean forceReload;
-
- public ConfigUserService(File realmFile) {
- this.realmFile = realmFile;
- }
-
- /**
- * Setup the user service.
- *
- * @param settings
- * @since 0.7.0
- */
- @Override
- public void setup(IStoredSettings settings) {
- }
-
- /**
- * Does the user service support changes to credentials?
- *
- * @return true or false
- * @since 1.0.0
- */
- @Override
- public boolean supportsCredentialChanges() {
- return true;
- }
-
- /**
- * Does the user service support changes to user display name?
- *
- * @return true or false
- * @since 1.0.0
- */
- @Override
- public boolean supportsDisplayNameChanges() {
- return true;
- }
-
- /**
- * Does the user service support changes to user email address?
- *
- * @return true or false
- * @since 1.0.0
- */
- @Override
- public boolean supportsEmailAddressChanges() {
- return true;
- }
-
- /**
- * Does the user service support changes to team memberships?
- *
- * @return true or false
- * @since 1.0.0
- */
- public boolean supportsTeamMembershipChanges() {
- return true;
- }
-
- /**
- * Does the user service support cookie authentication?
- *
- * @return true or false
- */
- @Override
- public boolean supportsCookies() {
- return true;
- }
-
- /**
- * Returns the cookie value for the specified user.
- *
- * @param model
- * @return cookie value
- */
- @Override
- public String getCookie(UserModel model) {
- if (!StringUtils.isEmpty(model.cookie)) {
- return model.cookie;
- }
- read();
- UserModel storedModel = users.get(model.username.toLowerCase());
- return storedModel.cookie;
- }
-
- /**
- * Authenticate a user based on their cookie.
- *
- * @param cookie
- * @return a user object or null
- */
- @Override
- public UserModel authenticate(char[] cookie) {
- String hash = new String(cookie);
- if (StringUtils.isEmpty(hash)) {
- return null;
- }
- read();
- UserModel model = null;
- if (cookies.containsKey(hash)) {
- model = cookies.get(hash);
- }
- return model;
- }
-
- /**
- * Authenticate a user based on a username and password.
- *
- * @param username
- * @param password
- * @return a user object or null
- */
- @Override
- public UserModel authenticate(String username, char[] password) {
- read();
- UserModel returnedUser = null;
- UserModel user = getUserModel(username);
- if (user == null) {
- return null;
- }
- if (user.password.startsWith(StringUtils.MD5_TYPE)) {
- // password digest
- String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
- if (user.password.equalsIgnoreCase(md5)) {
- returnedUser = user;
- }
- } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) {
- // username+password digest
- String md5 = StringUtils.COMBINED_MD5_TYPE
- + StringUtils.getMD5(username.toLowerCase() + new String(password));
- if (user.password.equalsIgnoreCase(md5)) {
- returnedUser = user;
- }
- } else if (user.password.equals(new String(password))) {
- // plain-text password
- returnedUser = user;
- }
- return returnedUser;
- }
-
- /**
- * Logout a user.
- *
- * @param user
- */
- @Override
- public void logout(UserModel user) {
- }
-
- /**
- * Retrieve the user object for the specified username.
- *
- * @param username
- * @return a user object or null
- */
- @Override
- public UserModel getUserModel(String username) {
- read();
- UserModel model = users.get(username.toLowerCase());
- if (model != null) {
- // clone the model, otherwise all changes to this object are
- // live and unpersisted
- model = DeepCopier.copy(model);
- }
- return model;
- }
-
- /**
- * Updates/writes a complete user object.
- *
- * @param model
- * @return true if update is successful
- */
- @Override
- public boolean updateUserModel(UserModel model) {
- return updateUserModel(model.username, model);
- }
-
- /**
- * Updates/writes all specified user objects.
- *
- * @param models a list of user models
- * @return true if update is successful
- * @since 1.2.0
- */
- @Override
- public boolean updateUserModels(List<UserModel> models) {
- try {
- read();
- for (UserModel model : models) {
- UserModel originalUser = users.remove(model.username.toLowerCase());
- users.put(model.username.toLowerCase(), model);
- // null check on "final" teams because JSON-sourced UserModel
- // can have a null teams object
- if (model.teams != null) {
- for (TeamModel team : model.teams) {
- TeamModel t = teams.get(team.name.toLowerCase());
- if (t == null) {
- // new team
- team.addUser(model.username);
- teams.put(team.name.toLowerCase(), team);
- } else {
- // do not clobber existing team definition
- // maybe because this is a federated user
- t.addUser(model.username);
- }
- }
-
- // check for implicit team removal
- if (originalUser != null) {
- for (TeamModel team : originalUser.teams) {
- if (!model.isTeamMember(team.name)) {
- team.removeUser(model.username);
- }
- }
- }
- }
- }
- write();
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to update user {0} models!", models.size()),
- t);
- }
- return false;
- }
-
- /**
- * Updates/writes and replaces a complete user object keyed by username.
- * This method allows for renaming a user.
- *
- * @param username
- * the old username
- * @param model
- * the user object to use for username
- * @return true if update is successful
- */
- @Override
- public boolean updateUserModel(String username, UserModel model) {
- UserModel originalUser = null;
- try {
- read();
- originalUser = users.remove(username.toLowerCase());
- users.put(model.username.toLowerCase(), model);
- // null check on "final" teams because JSON-sourced UserModel
- // can have a null teams object
- if (model.teams != null) {
- for (TeamModel team : model.teams) {
- TeamModel t = teams.get(team.name.toLowerCase());
- if (t == null) {
- // new team
- team.addUser(username);
- teams.put(team.name.toLowerCase(), team);
- } else {
- // do not clobber existing team definition
- // maybe because this is a federated user
- t.removeUser(username);
- t.addUser(model.username);
- }
- }
-
- // check for implicit team removal
- if (originalUser != null) {
- for (TeamModel team : originalUser.teams) {
- if (!model.isTeamMember(team.name)) {
- team.removeUser(username);
- }
- }
- }
- }
- write();
- return true;
- } catch (Throwable t) {
- if (originalUser != null) {
- // restore original user
- users.put(originalUser.username.toLowerCase(), originalUser);
- } else {
- // drop attempted add
- users.remove(model.username.toLowerCase());
- }
- logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
- t);
- }
- return false;
- }
-
- /**
- * Deletes the user object from the user service.
- *
- * @param model
- * @return true if successful
- */
- @Override
- public boolean deleteUserModel(UserModel model) {
- return deleteUser(model.username);
- }
-
- /**
- * Delete the user object with the specified username
- *
- * @param username
- * @return true if successful
- */
- @Override
- public boolean deleteUser(String username) {
- try {
- // Read realm file
- read();
- UserModel model = users.remove(username.toLowerCase());
- // remove user from team
- for (TeamModel team : model.teams) {
- TeamModel t = teams.get(team.name);
- if (t == null) {
- // new team
- team.removeUser(username);
- teams.put(team.name.toLowerCase(), team);
- } else {
- // existing team
- t.removeUser(username);
- }
- }
- write();
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to delete user {0}!", username), t);
- }
- return false;
- }
-
- /**
- * Returns the list of all teams available to the login service.
- *
- * @return list of all teams
- * @since 0.8.0
- */
- @Override
- public List<String> getAllTeamNames() {
- read();
- List<String> list = new ArrayList<String>(teams.keySet());
- Collections.sort(list);
- return list;
- }
-
- /**
- * Returns the list of all teams available to the login service.
- *
- * @return list of all teams
- * @since 0.8.0
- */
- @Override
- public List<TeamModel> getAllTeams() {
- read();
- List<TeamModel> list = new ArrayList<TeamModel>(teams.values());
- list = DeepCopier.copy(list);
- Collections.sort(list);
- return list;
- }
-
- /**
- * Returns the list of all users who are allowed to bypass the access
- * restriction placed on the specified repository.
- *
- * @param role
- * the repository name
- * @return list of all usernames that can bypass the access restriction
- */
- @Override
- public List<String> getTeamnamesForRepositoryRole(String role) {
- List<String> list = new ArrayList<String>();
- try {
- read();
- for (Map.Entry<String, TeamModel> entry : teams.entrySet()) {
- TeamModel model = entry.getValue();
- if (model.hasRepositoryPermission(role)) {
- list.add(model.name);
- }
- }
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t);
- }
- Collections.sort(list);
- return list;
- }
-
- /**
- * Sets the list of all teams who are allowed to bypass the access
- * restriction placed on the specified repository.
- *
- * @param role
- * the repository name
- * @param teamnames
- * @return true if successful
- */
- @Override
- public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) {
- try {
- Set<String> specifiedTeams = new HashSet<String>();
- for (String teamname : teamnames) {
- specifiedTeams.add(teamname.toLowerCase());
- }
-
- read();
-
- // identify teams which require add or remove role
- for (TeamModel team : teams.values()) {
- // team has role, check against revised team list
- if (specifiedTeams.contains(team.name.toLowerCase())) {
- team.addRepositoryPermission(role);
- } else {
- // remove role from team
- team.removeRepositoryPermission(role);
- }
- }
-
- // persist changes
- write();
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to set teams for role {0}!", role), t);
- }
- return false;
- }
-
- /**
- * Retrieve the team object for the specified team name.
- *
- * @param teamname
- * @return a team object or null
- * @since 0.8.0
- */
- @Override
- public TeamModel getTeamModel(String teamname) {
- read();
- TeamModel model = teams.get(teamname.toLowerCase());
- if (model != null) {
- // clone the model, otherwise all changes to this object are
- // live and unpersisted
- model = DeepCopier.copy(model);
- }
- return model;
- }
-
- /**
- * Updates/writes a complete team object.
- *
- * @param model
- * @return true if update is successful
- * @since 0.8.0
- */
- @Override
- public boolean updateTeamModel(TeamModel model) {
- return updateTeamModel(model.name, model);
- }
-
- /**
- * Updates/writes all specified team objects.
- *
- * @param models a list of team models
- * @return true if update is successful
- * @since 1.2.0
- */
- @Override
- public boolean updateTeamModels(List<TeamModel> models) {
- try {
- read();
- for (TeamModel team : models) {
- teams.put(team.name.toLowerCase(), team);
- }
- write();
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to update team {0} models!", models.size()), t);
- }
- return false;
- }
-
- /**
- * Updates/writes and replaces a complete team object keyed by teamname.
- * This method allows for renaming a team.
- *
- * @param teamname
- * the old teamname
- * @param model
- * the team object to use for teamname
- * @return true if update is successful
- * @since 0.8.0
- */
- @Override
- public boolean updateTeamModel(String teamname, TeamModel model) {
- TeamModel original = null;
- try {
- read();
- original = teams.remove(teamname.toLowerCase());
- teams.put(model.name.toLowerCase(), model);
- write();
- return true;
- } catch (Throwable t) {
- if (original != null) {
- // restore original team
- teams.put(original.name.toLowerCase(), original);
- } else {
- // drop attempted add
- teams.remove(model.name.toLowerCase());
- }
- logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t);
- }
- return false;
- }
-
- /**
- * Deletes the team object from the user service.
- *
- * @param model
- * @return true if successful
- * @since 0.8.0
- */
- @Override
- public boolean deleteTeamModel(TeamModel model) {
- return deleteTeam(model.name);
- }
-
- /**
- * Delete the team object with the specified teamname
- *
- * @param teamname
- * @return true if successful
- * @since 0.8.0
- */
- @Override
- public boolean deleteTeam(String teamname) {
- try {
- // Read realm file
- read();
- teams.remove(teamname.toLowerCase());
- write();
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t);
- }
- return false;
- }
-
- /**
- * Returns the list of all users available to the login service.
- *
- * @return list of all usernames
- */
- @Override
- public List<String> getAllUsernames() {
- read();
- List<String> list = new ArrayList<String>(users.keySet());
- Collections.sort(list);
- return list;
- }
-
- /**
- * Returns the list of all users available to the login service.
- *
- * @return list of all usernames
- */
- @Override
- public List<UserModel> getAllUsers() {
- read();
- List<UserModel> list = new ArrayList<UserModel>(users.values());
- list = DeepCopier.copy(list);
- Collections.sort(list);
- return list;
- }
-
- /**
- * Returns the list of all users who are allowed to bypass the access
- * restriction placed on the specified repository.
- *
- * @param role
- * the repository name
- * @return list of all usernames that can bypass the access restriction
- */
- @Override
- public List<String> getUsernamesForRepositoryRole(String role) {
- List<String> list = new ArrayList<String>();
- try {
- read();
- for (Map.Entry<String, UserModel> entry : users.entrySet()) {
- UserModel model = entry.getValue();
- if (model.hasRepositoryPermission(role)) {
- list.add(model.username);
- }
- }
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t);
- }
- Collections.sort(list);
- return list;
- }
-
- /**
- * Sets the list of all uses who are allowed to bypass the access
- * restriction placed on the specified repository.
- *
- * @param role
- * the repository name
- * @param usernames
- * @return true if successful
- */
- @Override
- @Deprecated
- public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
- try {
- Set<String> specifiedUsers = new HashSet<String>();
- for (String username : usernames) {
- specifiedUsers.add(username.toLowerCase());
- }
-
- read();
-
- // identify users which require add or remove role
- for (UserModel user : users.values()) {
- // user has role, check against revised user list
- if (specifiedUsers.contains(user.username.toLowerCase())) {
- user.addRepositoryPermission(role);
- } else {
- // remove role from user
- user.removeRepositoryPermission(role);
- }
- }
-
- // persist changes
- write();
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t);
- }
- return false;
- }
-
- /**
- * Renames a repository role.
- *
- * @param oldRole
- * @param newRole
- * @return true if successful
- */
- @Override
- public boolean renameRepositoryRole(String oldRole, String newRole) {
- try {
- read();
- // identify users which require role rename
- for (UserModel model : users.values()) {
- if (model.hasRepositoryPermission(oldRole)) {
- AccessPermission permission = model.removeRepositoryPermission(oldRole);
- model.setRepositoryPermission(newRole, permission);
- }
- }
-
- // identify teams which require role rename
- for (TeamModel model : teams.values()) {
- if (model.hasRepositoryPermission(oldRole)) {
- AccessPermission permission = model.removeRepositoryPermission(oldRole);
- model.setRepositoryPermission(newRole, permission);
- }
- }
- // persist changes
- write();
- return true;
- } catch (Throwable t) {
- logger.error(
- MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t);
- }
- return false;
- }
-
- /**
- * Removes a repository role from all users.
- *
- * @param role
- * @return true if successful
- */
- @Override
- public boolean deleteRepositoryRole(String role) {
- try {
- read();
-
- // identify users which require role rename
- for (UserModel user : users.values()) {
- user.removeRepositoryPermission(role);
- }
-
- // identify teams which require role rename
- for (TeamModel team : teams.values()) {
- team.removeRepositoryPermission(role);
- }
-
- // persist changes
- write();
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to delete role {0}!", role), t);
- }
- return false;
- }
-
- /**
- * Writes the properties file.
- *
- * @param properties
- * @throws IOException
- */
- private synchronized void write() throws IOException {
- // Write a temporary copy of the users file
- File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp");
-
- StoredConfig config = new FileBasedConfig(realmFileCopy, FS.detect());
-
- // write users
- for (UserModel model : users.values()) {
- if (!StringUtils.isEmpty(model.password)) {
- config.setString(USER, model.username, PASSWORD, model.password);
- }
- if (!StringUtils.isEmpty(model.cookie)) {
- config.setString(USER, model.username, COOKIE, model.cookie);
- }
- if (!StringUtils.isEmpty(model.displayName)) {
- config.setString(USER, model.username, DISPLAYNAME, model.displayName);
- }
- if (!StringUtils.isEmpty(model.emailAddress)) {
- config.setString(USER, model.username, EMAILADDRESS, model.emailAddress);
- }
- if (!StringUtils.isEmpty(model.organizationalUnit)) {
- config.setString(USER, model.username, ORGANIZATIONALUNIT, model.organizationalUnit);
- }
- if (!StringUtils.isEmpty(model.organization)) {
- config.setString(USER, model.username, ORGANIZATION, model.organization);
- }
- if (!StringUtils.isEmpty(model.locality)) {
- config.setString(USER, model.username, LOCALITY, model.locality);
- }
- if (!StringUtils.isEmpty(model.stateProvince)) {
- config.setString(USER, model.username, STATEPROVINCE, model.stateProvince);
- }
- if (!StringUtils.isEmpty(model.countryCode)) {
- config.setString(USER, model.username, COUNTRYCODE, model.countryCode);
- }
-
- // user roles
- List<String> roles = new ArrayList<String>();
- if (model.canAdmin) {
- roles.add(Constants.ADMIN_ROLE);
- }
- if (model.canFork) {
- roles.add(Constants.FORK_ROLE);
- }
- if (model.canCreate) {
- roles.add(Constants.CREATE_ROLE);
- }
- if (model.excludeFromFederation) {
- roles.add(Constants.NOT_FEDERATED_ROLE);
- }
- if (roles.size() == 0) {
- // we do this to ensure that user record with no password
- // is written. otherwise, StoredConfig optimizes that account
- // away. :(
- roles.add(Constants.NO_ROLE);
- }
- config.setStringList(USER, model.username, ROLE, roles);
-
- // discrete repository permissions
- if (model.permissions != null && !model.canAdmin) {
- List<String> permissions = new ArrayList<String>();
- for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) {
- if (entry.getValue().exceeds(AccessPermission.NONE)) {
- permissions.add(entry.getValue().asRole(entry.getKey()));
- }
- }
- config.setStringList(USER, model.username, REPOSITORY, permissions);
- }
- }
-
- // write teams
- for (TeamModel model : teams.values()) {
- // team roles
- List<String> roles = new ArrayList<String>();
- if (model.canAdmin) {
- roles.add(Constants.ADMIN_ROLE);
- }
- if (model.canFork) {
- roles.add(Constants.FORK_ROLE);
- }
- if (model.canCreate) {
- roles.add(Constants.CREATE_ROLE);
- }
- if (roles.size() == 0) {
- // we do this to ensure that team record is written.
- // Otherwise, StoredConfig might optimizes that record away.
- roles.add(Constants.NO_ROLE);
- }
- config.setStringList(TEAM, model.name, ROLE, roles);
-
- if (!model.canAdmin) {
- // write team permission for non-admin teams
- if (model.permissions == null) {
- // null check on "final" repositories because JSON-sourced TeamModel
- // can have a null repositories object
- if (!ArrayUtils.isEmpty(model.repositories)) {
- config.setStringList(TEAM, model.name, REPOSITORY, new ArrayList<String>(
- model.repositories));
- }
- } else {
- // discrete repository permissions
- List<String> permissions = new ArrayList<String>();
- for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) {
- if (entry.getValue().exceeds(AccessPermission.NONE)) {
- // code:repository (e.g. RW+:~james/myrepo.git
- permissions.add(entry.getValue().asRole(entry.getKey()));
- }
- }
- config.setStringList(TEAM, model.name, REPOSITORY, permissions);
- }
- }
-
- // null check on "final" users because JSON-sourced TeamModel
- // can have a null users object
- if (!ArrayUtils.isEmpty(model.users)) {
- config.setStringList(TEAM, model.name, USER, new ArrayList<String>(model.users));
- }
-
- // null check on "final" mailing lists because JSON-sourced
- // TeamModel can have a null users object
- if (!ArrayUtils.isEmpty(model.mailingLists)) {
- config.setStringList(TEAM, model.name, MAILINGLIST, new ArrayList<String>(
- model.mailingLists));
- }
-
- // null check on "final" preReceiveScripts because JSON-sourced
- // TeamModel can have a null preReceiveScripts object
- if (!ArrayUtils.isEmpty(model.preReceiveScripts)) {
- config.setStringList(TEAM, model.name, PRERECEIVE, model.preReceiveScripts);
- }
-
- // null check on "final" postReceiveScripts because JSON-sourced
- // TeamModel can have a null postReceiveScripts object
- if (!ArrayUtils.isEmpty(model.postReceiveScripts)) {
- config.setStringList(TEAM, model.name, POSTRECEIVE, model.postReceiveScripts);
- }
- }
-
- config.save();
- // manually set the forceReload flag because not all JVMs support real
- // millisecond resolution of lastModified. (issue-55)
- forceReload = true;
-
- // If the write is successful, delete the current file and rename
- // the temporary copy to the original filename.
- if (realmFileCopy.exists() && realmFileCopy.length() > 0) {
- if (realmFile.exists()) {
- if (!realmFile.delete()) {
- throw new IOException(MessageFormat.format("Failed to delete {0}!",
- realmFile.getAbsolutePath()));
- }
- }
- if (!realmFileCopy.renameTo(realmFile)) {
- throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!",
- realmFileCopy.getAbsolutePath(), realmFile.getAbsolutePath()));
- }
- } else {
- throw new IOException(MessageFormat.format("Failed to save {0}!",
- realmFileCopy.getAbsolutePath()));
- }
- }
-
- /**
- * Reads the realm file and rebuilds the in-memory lookup tables.
- */
- protected synchronized void read() {
- if (realmFile.exists() && (forceReload || (realmFile.lastModified() != lastModified))) {
- forceReload = false;
- lastModified = realmFile.lastModified();
- users.clear();
- cookies.clear();
- teams.clear();
-
- try {
- StoredConfig config = new FileBasedConfig(realmFile, FS.detect());
- config.load();
- Set<String> usernames = config.getSubsections(USER);
- for (String username : usernames) {
- UserModel user = new UserModel(username.toLowerCase());
- user.password = config.getString(USER, username, PASSWORD);
- user.displayName = config.getString(USER, username, DISPLAYNAME);
- user.emailAddress = config.getString(USER, username, EMAILADDRESS);
- user.organizationalUnit = config.getString(USER, username, ORGANIZATIONALUNIT);
- user.organization = config.getString(USER, username, ORGANIZATION);
- user.locality = config.getString(USER, username, LOCALITY);
- user.stateProvince = config.getString(USER, username, STATEPROVINCE);
- user.countryCode = config.getString(USER, username, COUNTRYCODE);
- user.cookie = config.getString(USER, username, COOKIE);
- if (StringUtils.isEmpty(user.cookie) && !StringUtils.isEmpty(user.password)) {
- user.cookie = StringUtils.getSHA1(user.username + user.password);
- }
-
- // user roles
- Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList(
- USER, username, ROLE)));
- user.canAdmin = roles.contains(Constants.ADMIN_ROLE);
- user.canFork = roles.contains(Constants.FORK_ROLE);
- user.canCreate = roles.contains(Constants.CREATE_ROLE);
- user.excludeFromFederation = roles.contains(Constants.NOT_FEDERATED_ROLE);
-
- // repository memberships
- if (!user.canAdmin) {
- // non-admin, read permissions
- Set<String> repositories = new HashSet<String>(Arrays.asList(config
- .getStringList(USER, username, REPOSITORY)));
- for (String repository : repositories) {
- user.addRepositoryPermission(repository);
- }
- }
-
- // update cache
- users.put(user.username, user);
- if (!StringUtils.isEmpty(user.cookie)) {
- cookies.put(user.cookie, user);
- }
- }
-
- // load the teams
- Set<String> teamnames = config.getSubsections(TEAM);
- for (String teamname : teamnames) {
- TeamModel team = new TeamModel(teamname);
- Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList(
- TEAM, teamname, ROLE)));
- team.canAdmin = roles.contains(Constants.ADMIN_ROLE);
- team.canFork = roles.contains(Constants.FORK_ROLE);
- team.canCreate = roles.contains(Constants.CREATE_ROLE);
-
- if (!team.canAdmin) {
- // non-admin team, read permissions
- team.addRepositoryPermissions(Arrays.asList(config.getStringList(TEAM, teamname,
- REPOSITORY)));
- }
- team.addUsers(Arrays.asList(config.getStringList(TEAM, teamname, USER)));
- team.addMailingLists(Arrays.asList(config.getStringList(TEAM, teamname,
- MAILINGLIST)));
- team.preReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM,
- teamname, PRERECEIVE)));
- team.postReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM,
- teamname, POSTRECEIVE)));
-
- teams.put(team.name.toLowerCase(), team);
-
- // set the teams on the users
- for (String user : team.users) {
- UserModel model = users.get(user);
- if (model != null) {
- model.teams.add(team);
- }
- }
- }
- } catch (Exception e) {
- logger.error(MessageFormat.format("Failed to read {0}", realmFile), e);
- }
- }
- }
-
- protected long lastModified() {
- return lastModified;
- }
-
- @Override
- public String toString() {
- return getClass().getSimpleName() + "(" + realmFile.getAbsolutePath() + ")";
- }
-}
+/* + * Copyright 2011 gitblit.com. + * + * 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.gitblit; + +import java.io.File; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.Constants.AccessPermission; +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.DeepCopier; +import com.gitblit.utils.StringUtils; + +/** + * ConfigUserService is Gitblit's default user service implementation since + * version 0.8.0. + * + * Users and their repository memberships are stored in a git-style config file + * which is cached and dynamically reloaded when modified. This file is + * plain-text, human-readable, and may be edited with a text editor. + * + * Additionally, this format allows for expansion of the user model without + * bringing in the complexity of a database. + * + * @author James Moger + * + */ +public class ConfigUserService implements IUserService { + + private static final String TEAM = "team"; + + private static final String USER = "user"; + + private static final String PASSWORD = "password"; + + private static final String DISPLAYNAME = "displayName"; + + private static final String EMAILADDRESS = "emailAddress"; + + private static final String ORGANIZATIONALUNIT = "organizationalUnit"; + + private static final String ORGANIZATION = "organization"; + + private static final String LOCALITY = "locality"; + + private static final String STATEPROVINCE = "stateProvince"; + + private static final String COUNTRYCODE = "countryCode"; + + private static final String COOKIE = "cookie"; + + private static final String REPOSITORY = "repository"; + + private static final String ROLE = "role"; + + private static final String MAILINGLIST = "mailingList"; + + private static final String PRERECEIVE = "preReceiveScript"; + + private static final String POSTRECEIVE = "postReceiveScript"; + + private final File realmFile; + + private final Logger logger = LoggerFactory.getLogger(ConfigUserService.class); + + private final Map<String, UserModel> users = new ConcurrentHashMap<String, UserModel>(); + + private final Map<String, UserModel> cookies = new ConcurrentHashMap<String, UserModel>(); + + private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>(); + + private volatile long lastModified; + + private volatile boolean forceReload; + + public ConfigUserService(File realmFile) { + this.realmFile = realmFile; + } + + /** + * Setup the user service. + * + * @param settings + * @since 0.7.0 + */ + @Override + public void setup(IStoredSettings settings) { + } + + /** + * Does the user service support changes to credentials? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsCredentialChanges() { + return true; + } + + /** + * Does the user service support changes to user display name? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsDisplayNameChanges() { + return true; + } + + /** + * Does the user service support changes to user email address? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsEmailAddressChanges() { + return true; + } + + /** + * Does the user service support changes to team memberships? + * + * @return true or false + * @since 1.0.0 + */ + public boolean supportsTeamMembershipChanges() { + return true; + } + + /** + * Does the user service support cookie authentication? + * + * @return true or false + */ + @Override + public boolean supportsCookies() { + return true; + } + + /** + * Returns the cookie value for the specified user. + * + * @param model + * @return cookie value + */ + @Override + public String getCookie(UserModel model) { + if (!StringUtils.isEmpty(model.cookie)) { + return model.cookie; + } + read(); + UserModel storedModel = users.get(model.username.toLowerCase()); + return storedModel.cookie; + } + + /** + * Authenticate a user based on their cookie. + * + * @param cookie + * @return a user object or null + */ + @Override + public UserModel authenticate(char[] cookie) { + String hash = new String(cookie); + if (StringUtils.isEmpty(hash)) { + return null; + } + read(); + UserModel model = null; + if (cookies.containsKey(hash)) { + model = cookies.get(hash); + } + return model; + } + + /** + * Authenticate a user based on a username and password. + * + * @param username + * @param password + * @return a user object or null + */ + @Override + public UserModel authenticate(String username, char[] password) { + read(); + UserModel returnedUser = null; + UserModel user = getUserModel(username); + if (user == null) { + return null; + } + if (user.password.startsWith(StringUtils.MD5_TYPE)) { + // password digest + String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password)); + if (user.password.equalsIgnoreCase(md5)) { + returnedUser = user; + } + } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) { + // username+password digest + String md5 = StringUtils.COMBINED_MD5_TYPE + + StringUtils.getMD5(username.toLowerCase() + new String(password)); + if (user.password.equalsIgnoreCase(md5)) { + returnedUser = user; + } + } else if (user.password.equals(new String(password))) { + // plain-text password + returnedUser = user; + } + return returnedUser; + } + + /** + * Logout a user. + * + * @param user + */ + @Override + public void logout(UserModel user) { + } + + /** + * Retrieve the user object for the specified username. + * + * @param username + * @return a user object or null + */ + @Override + public UserModel getUserModel(String username) { + read(); + UserModel model = users.get(username.toLowerCase()); + if (model != null) { + // clone the model, otherwise all changes to this object are + // live and unpersisted + model = DeepCopier.copy(model); + } + return model; + } + + /** + * Updates/writes a complete user object. + * + * @param model + * @return true if update is successful + */ + @Override + public boolean updateUserModel(UserModel model) { + return updateUserModel(model.username, model); + } + + /** + * Updates/writes all specified user objects. + * + * @param models a list of user models + * @return true if update is successful + * @since 1.2.0 + */ + @Override + public boolean updateUserModels(Collection<UserModel> models) { + try { + read(); + for (UserModel model : models) { + UserModel originalUser = users.remove(model.username.toLowerCase()); + users.put(model.username.toLowerCase(), model); + // null check on "final" teams because JSON-sourced UserModel + // can have a null teams object + if (model.teams != null) { + for (TeamModel team : model.teams) { + TeamModel t = teams.get(team.name.toLowerCase()); + if (t == null) { + // new team + team.addUser(model.username); + teams.put(team.name.toLowerCase(), team); + } else { + // do not clobber existing team definition + // maybe because this is a federated user + t.addUser(model.username); + } + } + + // check for implicit team removal + if (originalUser != null) { + for (TeamModel team : originalUser.teams) { + if (!model.isTeamMember(team.name)) { + team.removeUser(model.username); + } + } + } + } + } + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update user {0} models!", models.size()), + t); + } + return false; + } + + /** + * Updates/writes and replaces a complete user object keyed by username. + * This method allows for renaming a user. + * + * @param username + * the old username + * @param model + * the user object to use for username + * @return true if update is successful + */ + @Override + public boolean updateUserModel(String username, UserModel model) { + UserModel originalUser = null; + try { + read(); + originalUser = users.remove(username.toLowerCase()); + users.put(model.username.toLowerCase(), model); + // null check on "final" teams because JSON-sourced UserModel + // can have a null teams object + if (model.teams != null) { + for (TeamModel team : model.teams) { + TeamModel t = teams.get(team.name.toLowerCase()); + if (t == null) { + // new team + team.addUser(username); + teams.put(team.name.toLowerCase(), team); + } else { + // do not clobber existing team definition + // maybe because this is a federated user + t.removeUser(username); + t.addUser(model.username); + } + } + + // check for implicit team removal + if (originalUser != null) { + for (TeamModel team : originalUser.teams) { + if (!model.isTeamMember(team.name)) { + team.removeUser(username); + } + } + } + } + write(); + return true; + } catch (Throwable t) { + if (originalUser != null) { + // restore original user + users.put(originalUser.username.toLowerCase(), originalUser); + } else { + // drop attempted add + users.remove(model.username.toLowerCase()); + } + logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), + t); + } + return false; + } + + /** + * Deletes the user object from the user service. + * + * @param model + * @return true if successful + */ + @Override + public boolean deleteUserModel(UserModel model) { + return deleteUser(model.username); + } + + /** + * Delete the user object with the specified username + * + * @param username + * @return true if successful + */ + @Override + public boolean deleteUser(String username) { + try { + // Read realm file + read(); + UserModel model = users.remove(username.toLowerCase()); + // remove user from team + for (TeamModel team : model.teams) { + TeamModel t = teams.get(team.name); + if (t == null) { + // new team + team.removeUser(username); + teams.put(team.name.toLowerCase(), team); + } else { + // existing team + t.removeUser(username); + } + } + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete user {0}!", username), t); + } + return false; + } + + /** + * Returns the list of all teams available to the login service. + * + * @return list of all teams + * @since 0.8.0 + */ + @Override + public List<String> getAllTeamNames() { + read(); + List<String> list = new ArrayList<String>(teams.keySet()); + Collections.sort(list); + return list; + } + + /** + * Returns the list of all teams available to the login service. + * + * @return list of all teams + * @since 0.8.0 + */ + @Override + public List<TeamModel> getAllTeams() { + read(); + List<TeamModel> list = new ArrayList<TeamModel>(teams.values()); + list = DeepCopier.copy(list); + Collections.sort(list); + return list; + } + + /** + * Returns the list of all users who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @return list of all usernames that can bypass the access restriction + */ + @Override + public List<String> getTeamnamesForRepositoryRole(String role) { + List<String> list = new ArrayList<String>(); + try { + read(); + for (Map.Entry<String, TeamModel> entry : teams.entrySet()) { + TeamModel model = entry.getValue(); + if (model.hasRepositoryPermission(role)) { + list.add(model.name); + } + } + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t); + } + Collections.sort(list); + return list; + } + + /** + * Sets the list of all teams who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @param teamnames + * @return true if successful + */ + @Override + public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) { + try { + Set<String> specifiedTeams = new HashSet<String>(); + for (String teamname : teamnames) { + specifiedTeams.add(teamname.toLowerCase()); + } + + read(); + + // identify teams which require add or remove role + for (TeamModel team : teams.values()) { + // team has role, check against revised team list + if (specifiedTeams.contains(team.name.toLowerCase())) { + team.addRepositoryPermission(role); + } else { + // remove role from team + team.removeRepositoryPermission(role); + } + } + + // persist changes + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to set teams for role {0}!", role), t); + } + return false; + } + + /** + * Retrieve the team object for the specified team name. + * + * @param teamname + * @return a team object or null + * @since 0.8.0 + */ + @Override + public TeamModel getTeamModel(String teamname) { + read(); + TeamModel model = teams.get(teamname.toLowerCase()); + if (model != null) { + // clone the model, otherwise all changes to this object are + // live and unpersisted + model = DeepCopier.copy(model); + } + return model; + } + + /** + * Updates/writes a complete team object. + * + * @param model + * @return true if update is successful + * @since 0.8.0 + */ + @Override + public boolean updateTeamModel(TeamModel model) { + return updateTeamModel(model.name, model); + } + + /** + * Updates/writes all specified team objects. + * + * @param models a list of team models + * @return true if update is successful + * @since 1.2.0 + */ + @Override + public boolean updateTeamModels(Collection<TeamModel> models) { + try { + read(); + for (TeamModel team : models) { + teams.put(team.name.toLowerCase(), team); + } + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update team {0} models!", models.size()), t); + } + return false; + } + + /** + * Updates/writes and replaces a complete team object keyed by teamname. + * This method allows for renaming a team. + * + * @param teamname + * the old teamname + * @param model + * the team object to use for teamname + * @return true if update is successful + * @since 0.8.0 + */ + @Override + public boolean updateTeamModel(String teamname, TeamModel model) { + TeamModel original = null; + try { + read(); + original = teams.remove(teamname.toLowerCase()); + teams.put(model.name.toLowerCase(), model); + write(); + return true; + } catch (Throwable t) { + if (original != null) { + // restore original team + teams.put(original.name.toLowerCase(), original); + } else { + // drop attempted add + teams.remove(model.name.toLowerCase()); + } + logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t); + } + return false; + } + + /** + * Deletes the team object from the user service. + * + * @param model + * @return true if successful + * @since 0.8.0 + */ + @Override + public boolean deleteTeamModel(TeamModel model) { + return deleteTeam(model.name); + } + + /** + * Delete the team object with the specified teamname + * + * @param teamname + * @return true if successful + * @since 0.8.0 + */ + @Override + public boolean deleteTeam(String teamname) { + try { + // Read realm file + read(); + teams.remove(teamname.toLowerCase()); + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t); + } + return false; + } + + /** + * Returns the list of all users available to the login service. + * + * @return list of all usernames + */ + @Override + public List<String> getAllUsernames() { + read(); + List<String> list = new ArrayList<String>(users.keySet()); + Collections.sort(list); + return list; + } + + /** + * Returns the list of all users available to the login service. + * + * @return list of all usernames + */ + @Override + public List<UserModel> getAllUsers() { + read(); + List<UserModel> list = new ArrayList<UserModel>(users.values()); + list = DeepCopier.copy(list); + Collections.sort(list); + return list; + } + + /** + * Returns the list of all users who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @return list of all usernames that can bypass the access restriction + */ + @Override + public List<String> getUsernamesForRepositoryRole(String role) { + List<String> list = new ArrayList<String>(); + try { + read(); + for (Map.Entry<String, UserModel> entry : users.entrySet()) { + UserModel model = entry.getValue(); + if (model.hasRepositoryPermission(role)) { + list.add(model.username); + } + } + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t); + } + Collections.sort(list); + return list; + } + + /** + * Sets the list of all uses who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @param usernames + * @return true if successful + */ + @Override + @Deprecated + public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) { + try { + Set<String> specifiedUsers = new HashSet<String>(); + for (String username : usernames) { + specifiedUsers.add(username.toLowerCase()); + } + + read(); + + // identify users which require add or remove role + for (UserModel user : users.values()) { + // user has role, check against revised user list + if (specifiedUsers.contains(user.username.toLowerCase())) { + user.addRepositoryPermission(role); + } else { + // remove role from user + user.removeRepositoryPermission(role); + } + } + + // persist changes + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t); + } + return false; + } + + /** + * Renames a repository role. + * + * @param oldRole + * @param newRole + * @return true if successful + */ + @Override + public boolean renameRepositoryRole(String oldRole, String newRole) { + try { + read(); + // identify users which require role rename + for (UserModel model : users.values()) { + if (model.hasRepositoryPermission(oldRole)) { + AccessPermission permission = model.removeRepositoryPermission(oldRole); + model.setRepositoryPermission(newRole, permission); + } + } + + // identify teams which require role rename + for (TeamModel model : teams.values()) { + if (model.hasRepositoryPermission(oldRole)) { + AccessPermission permission = model.removeRepositoryPermission(oldRole); + model.setRepositoryPermission(newRole, permission); + } + } + // persist changes + write(); + return true; + } catch (Throwable t) { + logger.error( + MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t); + } + return false; + } + + /** + * Removes a repository role from all users. + * + * @param role + * @return true if successful + */ + @Override + public boolean deleteRepositoryRole(String role) { + try { + read(); + + // identify users which require role rename + for (UserModel user : users.values()) { + user.removeRepositoryPermission(role); + } + + // identify teams which require role rename + for (TeamModel team : teams.values()) { + team.removeRepositoryPermission(role); + } + + // persist changes + write(); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete role {0}!", role), t); + } + return false; + } + + /** + * Writes the properties file. + * + * @throws IOException + */ + private synchronized void write() throws IOException { + // Write a temporary copy of the users file + File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp"); + + StoredConfig config = new FileBasedConfig(realmFileCopy, FS.detect()); + + // write users + for (UserModel model : users.values()) { + if (!StringUtils.isEmpty(model.password)) { + config.setString(USER, model.username, PASSWORD, model.password); + } + if (!StringUtils.isEmpty(model.cookie)) { + config.setString(USER, model.username, COOKIE, model.cookie); + } + if (!StringUtils.isEmpty(model.displayName)) { + config.setString(USER, model.username, DISPLAYNAME, model.displayName); + } + if (!StringUtils.isEmpty(model.emailAddress)) { + config.setString(USER, model.username, EMAILADDRESS, model.emailAddress); + } + if (!StringUtils.isEmpty(model.organizationalUnit)) { + config.setString(USER, model.username, ORGANIZATIONALUNIT, model.organizationalUnit); + } + if (!StringUtils.isEmpty(model.organization)) { + config.setString(USER, model.username, ORGANIZATION, model.organization); + } + if (!StringUtils.isEmpty(model.locality)) { + config.setString(USER, model.username, LOCALITY, model.locality); + } + if (!StringUtils.isEmpty(model.stateProvince)) { + config.setString(USER, model.username, STATEPROVINCE, model.stateProvince); + } + if (!StringUtils.isEmpty(model.countryCode)) { + config.setString(USER, model.username, COUNTRYCODE, model.countryCode); + } + + // user roles + List<String> roles = new ArrayList<String>(); + if (model.canAdmin) { + roles.add(Constants.ADMIN_ROLE); + } + if (model.canFork) { + roles.add(Constants.FORK_ROLE); + } + if (model.canCreate) { + roles.add(Constants.CREATE_ROLE); + } + if (model.excludeFromFederation) { + roles.add(Constants.NOT_FEDERATED_ROLE); + } + if (roles.size() == 0) { + // we do this to ensure that user record with no password + // is written. otherwise, StoredConfig optimizes that account + // away. :( + roles.add(Constants.NO_ROLE); + } + config.setStringList(USER, model.username, ROLE, roles); + + // discrete repository permissions + if (model.permissions != null && !model.canAdmin) { + List<String> permissions = new ArrayList<String>(); + for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) { + if (entry.getValue().exceeds(AccessPermission.NONE)) { + permissions.add(entry.getValue().asRole(entry.getKey())); + } + } + config.setStringList(USER, model.username, REPOSITORY, permissions); + } + } + + // write teams + for (TeamModel model : teams.values()) { + // team roles + List<String> roles = new ArrayList<String>(); + if (model.canAdmin) { + roles.add(Constants.ADMIN_ROLE); + } + if (model.canFork) { + roles.add(Constants.FORK_ROLE); + } + if (model.canCreate) { + roles.add(Constants.CREATE_ROLE); + } + if (roles.size() == 0) { + // we do this to ensure that team record is written. + // Otherwise, StoredConfig might optimizes that record away. + roles.add(Constants.NO_ROLE); + } + config.setStringList(TEAM, model.name, ROLE, roles); + + if (!model.canAdmin) { + // write team permission for non-admin teams + if (model.permissions == null) { + // null check on "final" repositories because JSON-sourced TeamModel + // can have a null repositories object + if (!ArrayUtils.isEmpty(model.repositories)) { + config.setStringList(TEAM, model.name, REPOSITORY, new ArrayList<String>( + model.repositories)); + } + } else { + // discrete repository permissions + List<String> permissions = new ArrayList<String>(); + for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) { + if (entry.getValue().exceeds(AccessPermission.NONE)) { + // code:repository (e.g. RW+:~james/myrepo.git + permissions.add(entry.getValue().asRole(entry.getKey())); + } + } + config.setStringList(TEAM, model.name, REPOSITORY, permissions); + } + } + + // null check on "final" users because JSON-sourced TeamModel + // can have a null users object + if (!ArrayUtils.isEmpty(model.users)) { + config.setStringList(TEAM, model.name, USER, new ArrayList<String>(model.users)); + } + + // null check on "final" mailing lists because JSON-sourced + // TeamModel can have a null users object + if (!ArrayUtils.isEmpty(model.mailingLists)) { + config.setStringList(TEAM, model.name, MAILINGLIST, new ArrayList<String>( + model.mailingLists)); + } + + // null check on "final" preReceiveScripts because JSON-sourced + // TeamModel can have a null preReceiveScripts object + if (!ArrayUtils.isEmpty(model.preReceiveScripts)) { + config.setStringList(TEAM, model.name, PRERECEIVE, model.preReceiveScripts); + } + + // null check on "final" postReceiveScripts because JSON-sourced + // TeamModel can have a null postReceiveScripts object + if (!ArrayUtils.isEmpty(model.postReceiveScripts)) { + config.setStringList(TEAM, model.name, POSTRECEIVE, model.postReceiveScripts); + } + } + + config.save(); + // manually set the forceReload flag because not all JVMs support real + // millisecond resolution of lastModified. (issue-55) + forceReload = true; + + // If the write is successful, delete the current file and rename + // the temporary copy to the original filename. + if (realmFileCopy.exists() && realmFileCopy.length() > 0) { + if (realmFile.exists()) { + if (!realmFile.delete()) { + throw new IOException(MessageFormat.format("Failed to delete {0}!", + realmFile.getAbsolutePath())); + } + } + if (!realmFileCopy.renameTo(realmFile)) { + throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!", + realmFileCopy.getAbsolutePath(), realmFile.getAbsolutePath())); + } + } else { + throw new IOException(MessageFormat.format("Failed to save {0}!", + realmFileCopy.getAbsolutePath())); + } + } + + /** + * Reads the realm file and rebuilds the in-memory lookup tables. + */ + protected synchronized void read() { + if (realmFile.exists() && (forceReload || (realmFile.lastModified() != lastModified))) { + forceReload = false; + lastModified = realmFile.lastModified(); + users.clear(); + cookies.clear(); + teams.clear(); + + try { + StoredConfig config = new FileBasedConfig(realmFile, FS.detect()); + config.load(); + Set<String> usernames = config.getSubsections(USER); + for (String username : usernames) { + UserModel user = new UserModel(username.toLowerCase()); + user.password = config.getString(USER, username, PASSWORD); + user.displayName = config.getString(USER, username, DISPLAYNAME); + user.emailAddress = config.getString(USER, username, EMAILADDRESS); + user.organizationalUnit = config.getString(USER, username, ORGANIZATIONALUNIT); + user.organization = config.getString(USER, username, ORGANIZATION); + user.locality = config.getString(USER, username, LOCALITY); + user.stateProvince = config.getString(USER, username, STATEPROVINCE); + user.countryCode = config.getString(USER, username, COUNTRYCODE); + user.cookie = config.getString(USER, username, COOKIE); + if (StringUtils.isEmpty(user.cookie) && !StringUtils.isEmpty(user.password)) { + user.cookie = StringUtils.getSHA1(user.username + user.password); + } + + // user roles + Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList( + USER, username, ROLE))); + user.canAdmin = roles.contains(Constants.ADMIN_ROLE); + user.canFork = roles.contains(Constants.FORK_ROLE); + user.canCreate = roles.contains(Constants.CREATE_ROLE); + user.excludeFromFederation = roles.contains(Constants.NOT_FEDERATED_ROLE); + + // repository memberships + if (!user.canAdmin) { + // non-admin, read permissions + Set<String> repositories = new HashSet<String>(Arrays.asList(config + .getStringList(USER, username, REPOSITORY))); + for (String repository : repositories) { + user.addRepositoryPermission(repository); + } + } + + // update cache + users.put(user.username, user); + if (!StringUtils.isEmpty(user.cookie)) { + cookies.put(user.cookie, user); + } + } + + // load the teams + Set<String> teamnames = config.getSubsections(TEAM); + for (String teamname : teamnames) { + TeamModel team = new TeamModel(teamname); + Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList( + TEAM, teamname, ROLE))); + team.canAdmin = roles.contains(Constants.ADMIN_ROLE); + team.canFork = roles.contains(Constants.FORK_ROLE); + team.canCreate = roles.contains(Constants.CREATE_ROLE); + + if (!team.canAdmin) { + // non-admin team, read permissions + team.addRepositoryPermissions(Arrays.asList(config.getStringList(TEAM, teamname, + REPOSITORY))); + } + team.addUsers(Arrays.asList(config.getStringList(TEAM, teamname, USER))); + team.addMailingLists(Arrays.asList(config.getStringList(TEAM, teamname, + MAILINGLIST))); + team.preReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM, + teamname, PRERECEIVE))); + team.postReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM, + teamname, POSTRECEIVE))); + + teams.put(team.name.toLowerCase(), team); + + // set the teams on the users + for (String user : team.users) { + UserModel model = users.get(user); + if (model != null) { + model.teams.add(team); + } + } + } + } catch (Exception e) { + logger.error(MessageFormat.format("Failed to read {0}", realmFile), e); + } + } + } + + protected long lastModified() { + return lastModified; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + realmFile.getAbsolutePath() + ")"; + } +} diff --git a/src/com/gitblit/FileUserService.java b/src/com/gitblit/FileUserService.java index 056df820..a92cb508 100644 --- a/src/com/gitblit/FileUserService.java +++ b/src/com/gitblit/FileUserService.java @@ -1,1145 +1,1146 @@ -/*
- * Copyright 2011 gitblit.com.
- *
- * 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.gitblit;
-
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.gitblit.Constants.AccessPermission;
-import com.gitblit.models.TeamModel;
-import com.gitblit.models.UserModel;
-import com.gitblit.utils.ArrayUtils;
-import com.gitblit.utils.DeepCopier;
-import com.gitblit.utils.StringUtils;
-
-/**
- * FileUserService is Gitblit's original default user service implementation.
- *
- * Users and their repository memberships are stored in a simple properties file
- * which is cached and dynamically reloaded when modified.
- *
- * This class was deprecated in Gitblit 0.8.0 in favor of ConfigUserService
- * which is still a human-readable, editable, plain-text file but it is more
- * flexible for storing additional fields.
- *
- * @author James Moger
- *
- */
-@Deprecated
-public class FileUserService extends FileSettings implements IUserService {
-
- private final Logger logger = LoggerFactory.getLogger(FileUserService.class);
-
- private final Map<String, String> cookies = new ConcurrentHashMap<String, String>();
-
- private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>();
-
- public FileUserService(File realmFile) {
- super(realmFile.getAbsolutePath());
- }
-
- /**
- * Setup the user service.
- *
- * @param settings
- * @since 0.7.0
- */
- @Override
- public void setup(IStoredSettings settings) {
- }
-
- /**
- * Does the user service support changes to credentials?
- *
- * @return true or false
- * @since 1.0.0
- */
- @Override
- public boolean supportsCredentialChanges() {
- return true;
- }
-
- /**
- * Does the user service support changes to user display name?
- *
- * @return true or false
- * @since 1.0.0
- */
- @Override
- public boolean supportsDisplayNameChanges() {
- return false;
- }
-
- /**
- * Does the user service support changes to user email address?
- *
- * @return true or false
- * @since 1.0.0
- */
- @Override
- public boolean supportsEmailAddressChanges() {
- return false;
- }
-
- /**
- * Does the user service support changes to team memberships?
- *
- * @return true or false
- * @since 1.0.0
- */
- public boolean supportsTeamMembershipChanges() {
- return true;
- }
-
- /**
- * Does the user service support cookie authentication?
- *
- * @return true or false
- */
- @Override
- public boolean supportsCookies() {
- return true;
- }
-
- /**
- * Returns the cookie value for the specified user.
- *
- * @param model
- * @return cookie value
- */
- @Override
- public String getCookie(UserModel model) {
- if (!StringUtils.isEmpty(model.cookie)) {
- return model.cookie;
- }
- Properties allUsers = super.read();
- String value = allUsers.getProperty(model.username);
- String[] roles = value.split(",");
- String password = roles[0];
- String cookie = StringUtils.getSHA1(model.username + password);
- return cookie;
- }
-
- /**
- * Authenticate a user based on their cookie.
- *
- * @param cookie
- * @return a user object or null
- */
- @Override
- public UserModel authenticate(char[] cookie) {
- String hash = new String(cookie);
- if (StringUtils.isEmpty(hash)) {
- return null;
- }
- read();
- UserModel model = null;
- if (cookies.containsKey(hash)) {
- String username = cookies.get(hash);
- model = getUserModel(username);
- }
- return model;
- }
-
- /**
- * Authenticate a user based on a username and password.
- *
- * @param username
- * @param password
- * @return a user object or null
- */
- @Override
- public UserModel authenticate(String username, char[] password) {
- Properties allUsers = read();
- String userInfo = allUsers.getProperty(username);
- if (StringUtils.isEmpty(userInfo)) {
- return null;
- }
- UserModel returnedUser = null;
- UserModel user = getUserModel(username);
- if (user.password.startsWith(StringUtils.MD5_TYPE)) {
- // password digest
- String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
- if (user.password.equalsIgnoreCase(md5)) {
- returnedUser = user;
- }
- } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) {
- // username+password digest
- String md5 = StringUtils.COMBINED_MD5_TYPE
- + StringUtils.getMD5(username.toLowerCase() + new String(password));
- if (user.password.equalsIgnoreCase(md5)) {
- returnedUser = user;
- }
- } else if (user.password.equals(new String(password))) {
- // plain-text password
- returnedUser = user;
- }
- return returnedUser;
- }
-
- /**
- * Logout a user.
- *
- * @param user
- */
- @Override
- public void logout(UserModel user) {
- }
-
- /**
- * Retrieve the user object for the specified username.
- *
- * @param username
- * @return a user object or null
- */
- @Override
- public UserModel getUserModel(String username) {
- Properties allUsers = read();
- String userInfo = allUsers.getProperty(username.toLowerCase());
- if (userInfo == null) {
- return null;
- }
- UserModel model = new UserModel(username.toLowerCase());
- String[] userValues = userInfo.split(",");
- model.password = userValues[0];
- for (int i = 1; i < userValues.length; i++) {
- String role = userValues[i];
- switch (role.charAt(0)) {
- case '#':
- // Permissions
- if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
- model.canAdmin = true;
- } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) {
- model.canFork = true;
- } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) {
- model.canCreate = true;
- } else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) {
- model.excludeFromFederation = true;
- }
- break;
- default:
- model.addRepositoryPermission(role);
- }
- }
- // set the teams for the user
- for (TeamModel team : teams.values()) {
- if (team.hasUser(username)) {
- model.teams.add(DeepCopier.copy(team));
- }
- }
- return model;
- }
-
- /**
- * Updates/writes a complete user object.
- *
- * @param model
- * @return true if update is successful
- */
- @Override
- public boolean updateUserModel(UserModel model) {
- return updateUserModel(model.username, model);
- }
-
- /**
- * Updates/writes all specified user objects.
- *
- * @param model a list of user models
- * @return true if update is successful
- * @since 1.2.0
- */
- @Override
- public boolean updateUserModels(List<UserModel> models) {
- try {
- Properties allUsers = read();
- for (UserModel model : models) {
- updateUserCache(allUsers, model.username, model);
- }
- write(allUsers);
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to update {0} user models!", models.size()),
- t);
- }
- return false;
- }
-
- /**
- * Updates/writes and replaces a complete user object keyed by username.
- * This method allows for renaming a user.
- *
- * @param username
- * the old username
- * @param model
- * the user object to use for username
- * @return true if update is successful
- */
- @Override
- public boolean updateUserModel(String username, UserModel model) {
- try {
- Properties allUsers = read();
- updateUserCache(allUsers, username, model);
- write(allUsers);
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
- t);
- }
- return false;
- }
-
- /**
- * Updates/writes and replaces a complete user object keyed by username.
- * This method allows for renaming a user.
- *
- * @param username
- * the old username
- * @param model
- * the user object to use for username
- * @return true if update is successful
- */
- private boolean updateUserCache(Properties allUsers, String username, UserModel model) {
- try {
- UserModel oldUser = getUserModel(username);
- List<String> roles;
- if (model.permissions == null) {
- roles = new ArrayList<String>();
- } else {
- // discrete repository permissions
- roles = new ArrayList<String>();
- for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) {
- if (entry.getValue().exceeds(AccessPermission.NONE)) {
- // code:repository (e.g. RW+:~james/myrepo.git
- roles.add(entry.getValue().asRole(entry.getKey()));
- }
- }
- }
-
- // Permissions
- if (model.canAdmin) {
- roles.add(Constants.ADMIN_ROLE);
- }
- if (model.canFork) {
- roles.add(Constants.FORK_ROLE);
- }
- if (model.canCreate) {
- roles.add(Constants.CREATE_ROLE);
- }
- if (model.excludeFromFederation) {
- roles.add(Constants.NOT_FEDERATED_ROLE);
- }
-
- StringBuilder sb = new StringBuilder();
- if (!StringUtils.isEmpty(model.password)) {
- sb.append(model.password);
- }
- sb.append(',');
- for (String role : roles) {
- sb.append(role);
- sb.append(',');
- }
- // trim trailing comma
- sb.setLength(sb.length() - 1);
- allUsers.remove(username.toLowerCase());
- allUsers.put(model.username.toLowerCase(), sb.toString());
-
- // null check on "final" teams because JSON-sourced UserModel
- // can have a null teams object
- if (model.teams != null) {
- // update team cache
- for (TeamModel team : model.teams) {
- TeamModel t = getTeamModel(team.name);
- if (t == null) {
- // new team
- t = team;
- }
- t.removeUser(username);
- t.addUser(model.username);
- updateTeamCache(allUsers, t.name, t);
- }
-
- // check for implicit team removal
- if (oldUser != null) {
- for (TeamModel team : oldUser.teams) {
- if (!model.isTeamMember(team.name)) {
- team.removeUser(username);
- updateTeamCache(allUsers, team.name, team);
- }
- }
- }
- }
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
- t);
- }
- return false;
- }
-
- /**
- * Deletes the user object from the user service.
- *
- * @param model
- * @return true if successful
- */
- @Override
- public boolean deleteUserModel(UserModel model) {
- return deleteUser(model.username);
- }
-
- /**
- * Delete the user object with the specified username
- *
- * @param username
- * @return true if successful
- */
- @Override
- public boolean deleteUser(String username) {
- try {
- // Read realm file
- Properties allUsers = read();
- UserModel user = getUserModel(username);
- allUsers.remove(username);
- for (TeamModel team : user.teams) {
- TeamModel t = getTeamModel(team.name);
- if (t == null) {
- // new team
- t = team;
- }
- t.removeUser(username);
- updateTeamCache(allUsers, t.name, t);
- }
- write(allUsers);
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to delete user {0}!", username), t);
- }
- return false;
- }
-
- /**
- * Returns the list of all users available to the login service.
- *
- * @return list of all usernames
- */
- @Override
- public List<String> getAllUsernames() {
- Properties allUsers = read();
- List<String> list = new ArrayList<String>();
- for (String user : allUsers.stringPropertyNames()) {
- if (user.charAt(0) == '@') {
- // skip team user definitions
- continue;
- }
- list.add(user);
- }
- Collections.sort(list);
- return list;
- }
-
- /**
- * Returns the list of all users available to the login service.
- *
- * @return list of all usernames
- */
- @Override
- public List<UserModel> getAllUsers() {
- read();
- List<UserModel> list = new ArrayList<UserModel>();
- for (String username : getAllUsernames()) {
- list.add(getUserModel(username));
- }
- Collections.sort(list);
- return list;
- }
-
- /**
- * Returns the list of all users who are allowed to bypass the access
- * restriction placed on the specified repository.
- *
- * @param role
- * the repository name
- * @return list of all usernames that can bypass the access restriction
- */
- @Override
- public List<String> getUsernamesForRepositoryRole(String role) {
- List<String> list = new ArrayList<String>();
- try {
- Properties allUsers = read();
- for (String username : allUsers.stringPropertyNames()) {
- if (username.charAt(0) == '@') {
- continue;
- }
- String value = allUsers.getProperty(username);
- String[] values = value.split(",");
- // skip first value (password)
- for (int i = 1; i < values.length; i++) {
- String r = values[i];
- if (r.equalsIgnoreCase(role)) {
- list.add(username);
- break;
- }
- }
- }
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t);
- }
- Collections.sort(list);
- return list;
- }
-
- /**
- * Sets the list of all users who are allowed to bypass the access
- * restriction placed on the specified repository.
- *
- * @param role
- * the repository name
- * @param usernames
- * @return true if successful
- */
- @Override
- public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
- try {
- Set<String> specifiedUsers = new HashSet<String>(usernames);
- Set<String> needsAddRole = new HashSet<String>(specifiedUsers);
- Set<String> needsRemoveRole = new HashSet<String>();
-
- // identify users which require add and remove role
- Properties allUsers = read();
- for (String username : allUsers.stringPropertyNames()) {
- String value = allUsers.getProperty(username);
- String[] values = value.split(",");
- // skip first value (password)
- for (int i = 1; i < values.length; i++) {
- String r = values[i];
- if (r.equalsIgnoreCase(role)) {
- // user has role, check against revised user list
- if (specifiedUsers.contains(username)) {
- needsAddRole.remove(username);
- } else {
- // remove role from user
- needsRemoveRole.add(username);
- }
- break;
- }
- }
- }
-
- // add roles to users
- for (String user : needsAddRole) {
- String userValues = allUsers.getProperty(user);
- userValues += "," + role;
- allUsers.put(user, userValues);
- }
-
- // remove role from user
- for (String user : needsRemoveRole) {
- String[] values = allUsers.getProperty(user).split(",");
- String password = values[0];
- StringBuilder sb = new StringBuilder();
- sb.append(password);
- sb.append(',');
-
- // skip first value (password)
- for (int i = 1; i < values.length; i++) {
- String value = values[i];
- if (!value.equalsIgnoreCase(role)) {
- sb.append(value);
- sb.append(',');
- }
- }
- sb.setLength(sb.length() - 1);
-
- // update properties
- allUsers.put(user, sb.toString());
- }
-
- // persist changes
- write(allUsers);
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t);
- }
- return false;
- }
-
- /**
- * Renames a repository role.
- *
- * @param oldRole
- * @param newRole
- * @return true if successful
- */
- @Override
- public boolean renameRepositoryRole(String oldRole, String newRole) {
- try {
- Properties allUsers = read();
- Set<String> needsRenameRole = new HashSet<String>();
-
- // identify users which require role rename
- for (String username : allUsers.stringPropertyNames()) {
- String value = allUsers.getProperty(username);
- String[] roles = value.split(",");
- // skip first value (password)
- for (int i = 1; i < roles.length; i++) {
- String repository = AccessPermission.repositoryFromRole(roles[i]);
- if (repository.equalsIgnoreCase(oldRole)) {
- needsRenameRole.add(username);
- break;
- }
- }
- }
-
- // rename role for identified users
- for (String user : needsRenameRole) {
- String userValues = allUsers.getProperty(user);
- String[] values = userValues.split(",");
- String password = values[0];
- StringBuilder sb = new StringBuilder();
- sb.append(password);
- sb.append(',');
- sb.append(newRole);
- sb.append(',');
-
- // skip first value (password)
- for (int i = 1; i < values.length; i++) {
- String repository = AccessPermission.repositoryFromRole(values[i]);
- if (repository.equalsIgnoreCase(oldRole)) {
- AccessPermission permission = AccessPermission.permissionFromRole(values[i]);
- sb.append(permission.asRole(newRole));
- sb.append(',');
- } else {
- sb.append(values[i]);
- sb.append(',');
- }
- }
- sb.setLength(sb.length() - 1);
-
- // update properties
- allUsers.put(user, sb.toString());
- }
-
- // persist changes
- write(allUsers);
- return true;
- } catch (Throwable t) {
- logger.error(
- MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t);
- }
- return false;
- }
-
- /**
- * Removes a repository role from all users.
- *
- * @param role
- * @return true if successful
- */
- @Override
- public boolean deleteRepositoryRole(String role) {
- try {
- Properties allUsers = read();
- Set<String> needsDeleteRole = new HashSet<String>();
-
- // identify users which require role rename
- for (String username : allUsers.stringPropertyNames()) {
- String value = allUsers.getProperty(username);
- String[] roles = value.split(",");
- // skip first value (password)
- for (int i = 1; i < roles.length; i++) {
- String repository = AccessPermission.repositoryFromRole(roles[i]);
- if (repository.equalsIgnoreCase(role)) {
- needsDeleteRole.add(username);
- break;
- }
- }
- }
-
- // delete role for identified users
- for (String user : needsDeleteRole) {
- String userValues = allUsers.getProperty(user);
- String[] values = userValues.split(",");
- String password = values[0];
- StringBuilder sb = new StringBuilder();
- sb.append(password);
- sb.append(',');
- // skip first value (password)
- for (int i = 1; i < values.length; i++) {
- String repository = AccessPermission.repositoryFromRole(values[i]);
- if (!repository.equalsIgnoreCase(role)) {
- sb.append(values[i]);
- sb.append(',');
- }
- }
- sb.setLength(sb.length() - 1);
-
- // update properties
- allUsers.put(user, sb.toString());
- }
-
- // persist changes
- write(allUsers);
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to delete role {0}!", role), t);
- }
- return false;
- }
-
- /**
- * Writes the properties file.
- *
- * @param properties
- * @throws IOException
- */
- private void write(Properties properties) throws IOException {
- // Write a temporary copy of the users file
- File realmFileCopy = new File(propertiesFile.getAbsolutePath() + ".tmp");
- FileWriter writer = new FileWriter(realmFileCopy);
- properties
- .store(writer,
- " Gitblit realm file format:\n username=password,\\#permission,repository1,repository2...\n @teamname=!username1,!username2,!username3,repository1,repository2...");
- writer.close();
- // If the write is successful, delete the current file and rename
- // the temporary copy to the original filename.
- if (realmFileCopy.exists() && realmFileCopy.length() > 0) {
- if (propertiesFile.exists()) {
- if (!propertiesFile.delete()) {
- throw new IOException(MessageFormat.format("Failed to delete {0}!",
- propertiesFile.getAbsolutePath()));
- }
- }
- if (!realmFileCopy.renameTo(propertiesFile)) {
- throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!",
- realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath()));
- }
- } else {
- throw new IOException(MessageFormat.format("Failed to save {0}!",
- realmFileCopy.getAbsolutePath()));
- }
- }
-
- /**
- * Reads the properties file and rebuilds the in-memory cookie lookup table.
- */
- @Override
- protected synchronized Properties read() {
- long lastRead = lastModified();
- boolean reload = forceReload();
- Properties allUsers = super.read();
- if (reload || (lastRead != lastModified())) {
- // reload hash cache
- cookies.clear();
- teams.clear();
-
- for (String username : allUsers.stringPropertyNames()) {
- String value = allUsers.getProperty(username);
- String[] roles = value.split(",");
- if (username.charAt(0) == '@') {
- // team definition
- TeamModel team = new TeamModel(username.substring(1));
- List<String> repositories = new ArrayList<String>();
- List<String> users = new ArrayList<String>();
- List<String> mailingLists = new ArrayList<String>();
- List<String> preReceive = new ArrayList<String>();
- List<String> postReceive = new ArrayList<String>();
- for (String role : roles) {
- if (role.charAt(0) == '!') {
- users.add(role.substring(1));
- } else if (role.charAt(0) == '&') {
- mailingLists.add(role.substring(1));
- } else if (role.charAt(0) == '^') {
- preReceive.add(role.substring(1));
- } else if (role.charAt(0) == '%') {
- postReceive.add(role.substring(1));
- } else {
- switch (role.charAt(0)) {
- case '#':
- // Permissions
- if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
- team.canAdmin = true;
- } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) {
- team.canFork = true;
- } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) {
- team.canCreate = true;
- }
- break;
- default:
- repositories.add(role);
- }
- repositories.add(role);
- }
- }
- if (!team.canAdmin) {
- // only read permissions for non-admin teams
- team.addRepositoryPermissions(repositories);
- }
- team.addUsers(users);
- team.addMailingLists(mailingLists);
- team.preReceiveScripts.addAll(preReceive);
- team.postReceiveScripts.addAll(postReceive);
- teams.put(team.name.toLowerCase(), team);
- } else {
- // user definition
- String password = roles[0];
- cookies.put(StringUtils.getSHA1(username.toLowerCase() + password), username.toLowerCase());
- }
- }
- }
- return allUsers;
- }
-
- @Override
- public String toString() {
- return getClass().getSimpleName() + "(" + propertiesFile.getAbsolutePath() + ")";
- }
-
- /**
- * Returns the list of all teams available to the login service.
- *
- * @return list of all teams
- * @since 0.8.0
- */
- @Override
- public List<String> getAllTeamNames() {
- List<String> list = new ArrayList<String>(teams.keySet());
- Collections.sort(list);
- return list;
- }
-
- /**
- * Returns the list of all teams available to the login service.
- *
- * @return list of all teams
- * @since 0.8.0
- */
- @Override
- public List<TeamModel> getAllTeams() {
- List<TeamModel> list = new ArrayList<TeamModel>(teams.values());
- list = DeepCopier.copy(list);
- Collections.sort(list);
- return list;
- }
-
- /**
- * Returns the list of all teams who are allowed to bypass the access
- * restriction placed on the specified repository.
- *
- * @param role
- * the repository name
- * @return list of all teamnames that can bypass the access restriction
- */
- @Override
- public List<String> getTeamnamesForRepositoryRole(String role) {
- List<String> list = new ArrayList<String>();
- try {
- Properties allUsers = read();
- for (String team : allUsers.stringPropertyNames()) {
- if (team.charAt(0) != '@') {
- // skip users
- continue;
- }
- String value = allUsers.getProperty(team);
- String[] values = value.split(",");
- for (int i = 0; i < values.length; i++) {
- String r = values[i];
- if (r.equalsIgnoreCase(role)) {
- // strip leading @
- list.add(team.substring(1));
- break;
- }
- }
- }
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t);
- }
- Collections.sort(list);
- return list;
- }
-
- /**
- * Sets the list of all teams who are allowed to bypass the access
- * restriction placed on the specified repository.
- *
- * @param role
- * the repository name
- * @param teamnames
- * @return true if successful
- */
- @Override
- public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) {
- try {
- Set<String> specifiedTeams = new HashSet<String>(teamnames);
- Set<String> needsAddRole = new HashSet<String>(specifiedTeams);
- Set<String> needsRemoveRole = new HashSet<String>();
-
- // identify teams which require add and remove role
- Properties allUsers = read();
- for (String team : allUsers.stringPropertyNames()) {
- if (team.charAt(0) != '@') {
- // skip users
- continue;
- }
- String name = team.substring(1);
- String value = allUsers.getProperty(team);
- String[] values = value.split(",");
- for (int i = 0; i < values.length; i++) {
- String r = values[i];
- if (r.equalsIgnoreCase(role)) {
- // team has role, check against revised team list
- if (specifiedTeams.contains(name)) {
- needsAddRole.remove(name);
- } else {
- // remove role from team
- needsRemoveRole.add(name);
- }
- break;
- }
- }
- }
-
- // add roles to teams
- for (String name : needsAddRole) {
- String team = "@" + name;
- String teamValues = allUsers.getProperty(team);
- teamValues += "," + role;
- allUsers.put(team, teamValues);
- }
-
- // remove role from team
- for (String name : needsRemoveRole) {
- String team = "@" + name;
- String[] values = allUsers.getProperty(team).split(",");
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < values.length; i++) {
- String value = values[i];
- if (!value.equalsIgnoreCase(role)) {
- sb.append(value);
- sb.append(',');
- }
- }
- sb.setLength(sb.length() - 1);
-
- // update properties
- allUsers.put(team, sb.toString());
- }
-
- // persist changes
- write(allUsers);
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to set teamnames for role {0}!", role), t);
- }
- return false;
- }
-
- /**
- * Retrieve the team object for the specified team name.
- *
- * @param teamname
- * @return a team object or null
- * @since 0.8.0
- */
- @Override
- public TeamModel getTeamModel(String teamname) {
- read();
- TeamModel team = teams.get(teamname.toLowerCase());
- if (team != null) {
- // clone the model, otherwise all changes to this object are
- // live and unpersisted
- team = DeepCopier.copy(team);
- }
- return team;
- }
-
- /**
- * Updates/writes a complete team object.
- *
- * @param model
- * @return true if update is successful
- * @since 0.8.0
- */
- @Override
- public boolean updateTeamModel(TeamModel model) {
- return updateTeamModel(model.name, model);
- }
-
- /**
- * Updates/writes all specified team objects.
- *
- * @param models a list of team models
- * @return true if update is successful
- * @since 1.2.0
- */
- public boolean updateTeamModels(List<TeamModel> models) {
- try {
- Properties allUsers = read();
- for (TeamModel model : models) {
- updateTeamCache(allUsers, model.name, model);
- }
- write(allUsers);
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to update {0} team models!", models.size()), t);
- }
- return false;
- }
-
- /**
- * Updates/writes and replaces a complete team object keyed by teamname.
- * This method allows for renaming a team.
- *
- * @param teamname
- * the old teamname
- * @param model
- * the team object to use for teamname
- * @return true if update is successful
- * @since 0.8.0
- */
- @Override
- public boolean updateTeamModel(String teamname, TeamModel model) {
- try {
- Properties allUsers = read();
- updateTeamCache(allUsers, teamname, model);
- write(allUsers);
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t);
- }
- return false;
- }
-
- private void updateTeamCache(Properties allUsers, String teamname, TeamModel model) {
- StringBuilder sb = new StringBuilder();
- List<String> roles;
- if (model.permissions == null) {
- // legacy, use repository list
- if (model.repositories != null) {
- roles = new ArrayList<String>(model.repositories);
- } else {
- roles = new ArrayList<String>();
- }
- } else {
- // discrete repository permissions
- roles = new ArrayList<String>();
- for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) {
- if (entry.getValue().exceeds(AccessPermission.NONE)) {
- // code:repository (e.g. RW+:~james/myrepo.git
- roles.add(entry.getValue().asRole(entry.getKey()));
- }
- }
- }
-
- // Permissions
- if (model.canAdmin) {
- roles.add(Constants.ADMIN_ROLE);
- }
- if (model.canFork) {
- roles.add(Constants.FORK_ROLE);
- }
- if (model.canCreate) {
- roles.add(Constants.CREATE_ROLE);
- }
-
- for (String role : roles) {
- sb.append(role);
- sb.append(',');
- }
-
- if (!ArrayUtils.isEmpty(model.users)) {
- for (String user : model.users) {
- sb.append('!');
- sb.append(user);
- sb.append(',');
- }
- }
- if (!ArrayUtils.isEmpty(model.mailingLists)) {
- for (String address : model.mailingLists) {
- sb.append('&');
- sb.append(address);
- sb.append(',');
- }
- }
- if (!ArrayUtils.isEmpty(model.preReceiveScripts)) {
- for (String script : model.preReceiveScripts) {
- sb.append('^');
- sb.append(script);
- sb.append(',');
- }
- }
- if (!ArrayUtils.isEmpty(model.postReceiveScripts)) {
- for (String script : model.postReceiveScripts) {
- sb.append('%');
- sb.append(script);
- sb.append(',');
- }
- }
- // trim trailing comma
- sb.setLength(sb.length() - 1);
- allUsers.remove("@" + teamname);
- allUsers.put("@" + model.name, sb.toString());
-
- // update team cache
- teams.remove(teamname.toLowerCase());
- teams.put(model.name.toLowerCase(), model);
- }
-
- /**
- * Deletes the team object from the user service.
- *
- * @param model
- * @return true if successful
- * @since 0.8.0
- */
- @Override
- public boolean deleteTeamModel(TeamModel model) {
- return deleteTeam(model.name);
- }
-
- /**
- * Delete the team object with the specified teamname
- *
- * @param teamname
- * @return true if successful
- * @since 0.8.0
- */
- @Override
- public boolean deleteTeam(String teamname) {
- Properties allUsers = read();
- teams.remove(teamname.toLowerCase());
- allUsers.remove("@" + teamname);
- try {
- write(allUsers);
- return true;
- } catch (Throwable t) {
- logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t);
- }
- return false;
- }
-}
+/* + * Copyright 2011 gitblit.com. + * + * 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.gitblit; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.Constants.AccessPermission; +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; +import com.gitblit.utils.ArrayUtils; +import com.gitblit.utils.DeepCopier; +import com.gitblit.utils.StringUtils; + +/** + * FileUserService is Gitblit's original default user service implementation. + * + * Users and their repository memberships are stored in a simple properties file + * which is cached and dynamically reloaded when modified. + * + * This class was deprecated in Gitblit 0.8.0 in favor of ConfigUserService + * which is still a human-readable, editable, plain-text file but it is more + * flexible for storing additional fields. + * + * @author James Moger + * + */ +@Deprecated +public class FileUserService extends FileSettings implements IUserService { + + private final Logger logger = LoggerFactory.getLogger(FileUserService.class); + + private final Map<String, String> cookies = new ConcurrentHashMap<String, String>(); + + private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>(); + + public FileUserService(File realmFile) { + super(realmFile.getAbsolutePath()); + } + + /** + * Setup the user service. + * + * @param settings + * @since 0.7.0 + */ + @Override + public void setup(IStoredSettings settings) { + } + + /** + * Does the user service support changes to credentials? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsCredentialChanges() { + return true; + } + + /** + * Does the user service support changes to user display name? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsDisplayNameChanges() { + return false; + } + + /** + * Does the user service support changes to user email address? + * + * @return true or false + * @since 1.0.0 + */ + @Override + public boolean supportsEmailAddressChanges() { + return false; + } + + /** + * Does the user service support changes to team memberships? + * + * @return true or false + * @since 1.0.0 + */ + public boolean supportsTeamMembershipChanges() { + return true; + } + + /** + * Does the user service support cookie authentication? + * + * @return true or false + */ + @Override + public boolean supportsCookies() { + return true; + } + + /** + * Returns the cookie value for the specified user. + * + * @param model + * @return cookie value + */ + @Override + public String getCookie(UserModel model) { + if (!StringUtils.isEmpty(model.cookie)) { + return model.cookie; + } + Properties allUsers = super.read(); + String value = allUsers.getProperty(model.username); + String[] roles = value.split(","); + String password = roles[0]; + String cookie = StringUtils.getSHA1(model.username + password); + return cookie; + } + + /** + * Authenticate a user based on their cookie. + * + * @param cookie + * @return a user object or null + */ + @Override + public UserModel authenticate(char[] cookie) { + String hash = new String(cookie); + if (StringUtils.isEmpty(hash)) { + return null; + } + read(); + UserModel model = null; + if (cookies.containsKey(hash)) { + String username = cookies.get(hash); + model = getUserModel(username); + } + return model; + } + + /** + * Authenticate a user based on a username and password. + * + * @param username + * @param password + * @return a user object or null + */ + @Override + public UserModel authenticate(String username, char[] password) { + Properties allUsers = read(); + String userInfo = allUsers.getProperty(username); + if (StringUtils.isEmpty(userInfo)) { + return null; + } + UserModel returnedUser = null; + UserModel user = getUserModel(username); + if (user.password.startsWith(StringUtils.MD5_TYPE)) { + // password digest + String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password)); + if (user.password.equalsIgnoreCase(md5)) { + returnedUser = user; + } + } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) { + // username+password digest + String md5 = StringUtils.COMBINED_MD5_TYPE + + StringUtils.getMD5(username.toLowerCase() + new String(password)); + if (user.password.equalsIgnoreCase(md5)) { + returnedUser = user; + } + } else if (user.password.equals(new String(password))) { + // plain-text password + returnedUser = user; + } + return returnedUser; + } + + /** + * Logout a user. + * + * @param user + */ + @Override + public void logout(UserModel user) { + } + + /** + * Retrieve the user object for the specified username. + * + * @param username + * @return a user object or null + */ + @Override + public UserModel getUserModel(String username) { + Properties allUsers = read(); + String userInfo = allUsers.getProperty(username.toLowerCase()); + if (userInfo == null) { + return null; + } + UserModel model = new UserModel(username.toLowerCase()); + String[] userValues = userInfo.split(","); + model.password = userValues[0]; + for (int i = 1; i < userValues.length; i++) { + String role = userValues[i]; + switch (role.charAt(0)) { + case '#': + // Permissions + if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) { + model.canAdmin = true; + } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) { + model.canFork = true; + } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) { + model.canCreate = true; + } else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) { + model.excludeFromFederation = true; + } + break; + default: + model.addRepositoryPermission(role); + } + } + // set the teams for the user + for (TeamModel team : teams.values()) { + if (team.hasUser(username)) { + model.teams.add(DeepCopier.copy(team)); + } + } + return model; + } + + /** + * Updates/writes a complete user object. + * + * @param model + * @return true if update is successful + */ + @Override + public boolean updateUserModel(UserModel model) { + return updateUserModel(model.username, model); + } + + /** + * Updates/writes all specified user objects. + * + * @param models a list of user models + * @return true if update is successful + * @since 1.2.0 + */ + @Override + public boolean updateUserModels(Collection<UserModel> models) { + try { + Properties allUsers = read(); + for (UserModel model : models) { + updateUserCache(allUsers, model.username, model); + } + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update {0} user models!", models.size()), + t); + } + return false; + } + + /** + * Updates/writes and replaces a complete user object keyed by username. + * This method allows for renaming a user. + * + * @param username + * the old username + * @param model + * the user object to use for username + * @return true if update is successful + */ + @Override + public boolean updateUserModel(String username, UserModel model) { + try { + Properties allUsers = read(); + updateUserCache(allUsers, username, model); + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), + t); + } + return false; + } + + /** + * Updates/writes and replaces a complete user object keyed by username. + * This method allows for renaming a user. + * + * @param username + * the old username + * @param model + * the user object to use for username + * @return true if update is successful + */ + private boolean updateUserCache(Properties allUsers, String username, UserModel model) { + try { + UserModel oldUser = getUserModel(username); + List<String> roles; + if (model.permissions == null) { + roles = new ArrayList<String>(); + } else { + // discrete repository permissions + roles = new ArrayList<String>(); + for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) { + if (entry.getValue().exceeds(AccessPermission.NONE)) { + // code:repository (e.g. RW+:~james/myrepo.git + roles.add(entry.getValue().asRole(entry.getKey())); + } + } + } + + // Permissions + if (model.canAdmin) { + roles.add(Constants.ADMIN_ROLE); + } + if (model.canFork) { + roles.add(Constants.FORK_ROLE); + } + if (model.canCreate) { + roles.add(Constants.CREATE_ROLE); + } + if (model.excludeFromFederation) { + roles.add(Constants.NOT_FEDERATED_ROLE); + } + + StringBuilder sb = new StringBuilder(); + if (!StringUtils.isEmpty(model.password)) { + sb.append(model.password); + } + sb.append(','); + for (String role : roles) { + sb.append(role); + sb.append(','); + } + // trim trailing comma + sb.setLength(sb.length() - 1); + allUsers.remove(username.toLowerCase()); + allUsers.put(model.username.toLowerCase(), sb.toString()); + + // null check on "final" teams because JSON-sourced UserModel + // can have a null teams object + if (model.teams != null) { + // update team cache + for (TeamModel team : model.teams) { + TeamModel t = getTeamModel(team.name); + if (t == null) { + // new team + t = team; + } + t.removeUser(username); + t.addUser(model.username); + updateTeamCache(allUsers, t.name, t); + } + + // check for implicit team removal + if (oldUser != null) { + for (TeamModel team : oldUser.teams) { + if (!model.isTeamMember(team.name)) { + team.removeUser(username); + updateTeamCache(allUsers, team.name, team); + } + } + } + } + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), + t); + } + return false; + } + + /** + * Deletes the user object from the user service. + * + * @param model + * @return true if successful + */ + @Override + public boolean deleteUserModel(UserModel model) { + return deleteUser(model.username); + } + + /** + * Delete the user object with the specified username + * + * @param username + * @return true if successful + */ + @Override + public boolean deleteUser(String username) { + try { + // Read realm file + Properties allUsers = read(); + UserModel user = getUserModel(username); + allUsers.remove(username); + for (TeamModel team : user.teams) { + TeamModel t = getTeamModel(team.name); + if (t == null) { + // new team + t = team; + } + t.removeUser(username); + updateTeamCache(allUsers, t.name, t); + } + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete user {0}!", username), t); + } + return false; + } + + /** + * Returns the list of all users available to the login service. + * + * @return list of all usernames + */ + @Override + public List<String> getAllUsernames() { + Properties allUsers = read(); + List<String> list = new ArrayList<String>(); + for (String user : allUsers.stringPropertyNames()) { + if (user.charAt(0) == '@') { + // skip team user definitions + continue; + } + list.add(user); + } + Collections.sort(list); + return list; + } + + /** + * Returns the list of all users available to the login service. + * + * @return list of all usernames + */ + @Override + public List<UserModel> getAllUsers() { + read(); + List<UserModel> list = new ArrayList<UserModel>(); + for (String username : getAllUsernames()) { + list.add(getUserModel(username)); + } + Collections.sort(list); + return list; + } + + /** + * Returns the list of all users who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @return list of all usernames that can bypass the access restriction + */ + @Override + public List<String> getUsernamesForRepositoryRole(String role) { + List<String> list = new ArrayList<String>(); + try { + Properties allUsers = read(); + for (String username : allUsers.stringPropertyNames()) { + if (username.charAt(0) == '@') { + continue; + } + String value = allUsers.getProperty(username); + String[] values = value.split(","); + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String r = values[i]; + if (r.equalsIgnoreCase(role)) { + list.add(username); + break; + } + } + } + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t); + } + Collections.sort(list); + return list; + } + + /** + * Sets the list of all users who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @param usernames + * @return true if successful + */ + @Override + public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) { + try { + Set<String> specifiedUsers = new HashSet<String>(usernames); + Set<String> needsAddRole = new HashSet<String>(specifiedUsers); + Set<String> needsRemoveRole = new HashSet<String>(); + + // identify users which require add and remove role + Properties allUsers = read(); + for (String username : allUsers.stringPropertyNames()) { + String value = allUsers.getProperty(username); + String[] values = value.split(","); + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String r = values[i]; + if (r.equalsIgnoreCase(role)) { + // user has role, check against revised user list + if (specifiedUsers.contains(username)) { + needsAddRole.remove(username); + } else { + // remove role from user + needsRemoveRole.add(username); + } + break; + } + } + } + + // add roles to users + for (String user : needsAddRole) { + String userValues = allUsers.getProperty(user); + userValues += "," + role; + allUsers.put(user, userValues); + } + + // remove role from user + for (String user : needsRemoveRole) { + String[] values = allUsers.getProperty(user).split(","); + String password = values[0]; + StringBuilder sb = new StringBuilder(); + sb.append(password); + sb.append(','); + + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String value = values[i]; + if (!value.equalsIgnoreCase(role)) { + sb.append(value); + sb.append(','); + } + } + sb.setLength(sb.length() - 1); + + // update properties + allUsers.put(user, sb.toString()); + } + + // persist changes + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t); + } + return false; + } + + /** + * Renames a repository role. + * + * @param oldRole + * @param newRole + * @return true if successful + */ + @Override + public boolean renameRepositoryRole(String oldRole, String newRole) { + try { + Properties allUsers = read(); + Set<String> needsRenameRole = new HashSet<String>(); + + // identify users which require role rename + for (String username : allUsers.stringPropertyNames()) { + String value = allUsers.getProperty(username); + String[] roles = value.split(","); + // skip first value (password) + for (int i = 1; i < roles.length; i++) { + String repository = AccessPermission.repositoryFromRole(roles[i]); + if (repository.equalsIgnoreCase(oldRole)) { + needsRenameRole.add(username); + break; + } + } + } + + // rename role for identified users + for (String user : needsRenameRole) { + String userValues = allUsers.getProperty(user); + String[] values = userValues.split(","); + String password = values[0]; + StringBuilder sb = new StringBuilder(); + sb.append(password); + sb.append(','); + sb.append(newRole); + sb.append(','); + + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String repository = AccessPermission.repositoryFromRole(values[i]); + if (repository.equalsIgnoreCase(oldRole)) { + AccessPermission permission = AccessPermission.permissionFromRole(values[i]); + sb.append(permission.asRole(newRole)); + sb.append(','); + } else { + sb.append(values[i]); + sb.append(','); + } + } + sb.setLength(sb.length() - 1); + + // update properties + allUsers.put(user, sb.toString()); + } + + // persist changes + write(allUsers); + return true; + } catch (Throwable t) { + logger.error( + MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t); + } + return false; + } + + /** + * Removes a repository role from all users. + * + * @param role + * @return true if successful + */ + @Override + public boolean deleteRepositoryRole(String role) { + try { + Properties allUsers = read(); + Set<String> needsDeleteRole = new HashSet<String>(); + + // identify users which require role rename + for (String username : allUsers.stringPropertyNames()) { + String value = allUsers.getProperty(username); + String[] roles = value.split(","); + // skip first value (password) + for (int i = 1; i < roles.length; i++) { + String repository = AccessPermission.repositoryFromRole(roles[i]); + if (repository.equalsIgnoreCase(role)) { + needsDeleteRole.add(username); + break; + } + } + } + + // delete role for identified users + for (String user : needsDeleteRole) { + String userValues = allUsers.getProperty(user); + String[] values = userValues.split(","); + String password = values[0]; + StringBuilder sb = new StringBuilder(); + sb.append(password); + sb.append(','); + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String repository = AccessPermission.repositoryFromRole(values[i]); + if (!repository.equalsIgnoreCase(role)) { + sb.append(values[i]); + sb.append(','); + } + } + sb.setLength(sb.length() - 1); + + // update properties + allUsers.put(user, sb.toString()); + } + + // persist changes + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete role {0}!", role), t); + } + return false; + } + + /** + * Writes the properties file. + * + * @param properties + * @throws IOException + */ + private void write(Properties properties) throws IOException { + // Write a temporary copy of the users file + File realmFileCopy = new File(propertiesFile.getAbsolutePath() + ".tmp"); + FileWriter writer = new FileWriter(realmFileCopy); + properties + .store(writer, + " Gitblit realm file format:\n username=password,\\#permission,repository1,repository2...\n @teamname=!username1,!username2,!username3,repository1,repository2..."); + writer.close(); + // If the write is successful, delete the current file and rename + // the temporary copy to the original filename. + if (realmFileCopy.exists() && realmFileCopy.length() > 0) { + if (propertiesFile.exists()) { + if (!propertiesFile.delete()) { + throw new IOException(MessageFormat.format("Failed to delete {0}!", + propertiesFile.getAbsolutePath())); + } + } + if (!realmFileCopy.renameTo(propertiesFile)) { + throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!", + realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath())); + } + } else { + throw new IOException(MessageFormat.format("Failed to save {0}!", + realmFileCopy.getAbsolutePath())); + } + } + + /** + * Reads the properties file and rebuilds the in-memory cookie lookup table. + */ + @Override + protected synchronized Properties read() { + long lastRead = lastModified(); + boolean reload = forceReload(); + Properties allUsers = super.read(); + if (reload || (lastRead != lastModified())) { + // reload hash cache + cookies.clear(); + teams.clear(); + + for (String username : allUsers.stringPropertyNames()) { + String value = allUsers.getProperty(username); + String[] roles = value.split(","); + if (username.charAt(0) == '@') { + // team definition + TeamModel team = new TeamModel(username.substring(1)); + List<String> repositories = new ArrayList<String>(); + List<String> users = new ArrayList<String>(); + List<String> mailingLists = new ArrayList<String>(); + List<String> preReceive = new ArrayList<String>(); + List<String> postReceive = new ArrayList<String>(); + for (String role : roles) { + if (role.charAt(0) == '!') { + users.add(role.substring(1)); + } else if (role.charAt(0) == '&') { + mailingLists.add(role.substring(1)); + } else if (role.charAt(0) == '^') { + preReceive.add(role.substring(1)); + } else if (role.charAt(0) == '%') { + postReceive.add(role.substring(1)); + } else { + switch (role.charAt(0)) { + case '#': + // Permissions + if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) { + team.canAdmin = true; + } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) { + team.canFork = true; + } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) { + team.canCreate = true; + } + break; + default: + repositories.add(role); + } + repositories.add(role); + } + } + if (!team.canAdmin) { + // only read permissions for non-admin teams + team.addRepositoryPermissions(repositories); + } + team.addUsers(users); + team.addMailingLists(mailingLists); + team.preReceiveScripts.addAll(preReceive); + team.postReceiveScripts.addAll(postReceive); + teams.put(team.name.toLowerCase(), team); + } else { + // user definition + String password = roles[0]; + cookies.put(StringUtils.getSHA1(username.toLowerCase() + password), username.toLowerCase()); + } + } + } + return allUsers; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + propertiesFile.getAbsolutePath() + ")"; + } + + /** + * Returns the list of all teams available to the login service. + * + * @return list of all teams + * @since 0.8.0 + */ + @Override + public List<String> getAllTeamNames() { + List<String> list = new ArrayList<String>(teams.keySet()); + Collections.sort(list); + return list; + } + + /** + * Returns the list of all teams available to the login service. + * + * @return list of all teams + * @since 0.8.0 + */ + @Override + public List<TeamModel> getAllTeams() { + List<TeamModel> list = new ArrayList<TeamModel>(teams.values()); + list = DeepCopier.copy(list); + Collections.sort(list); + return list; + } + + /** + * Returns the list of all teams who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @return list of all teamnames that can bypass the access restriction + */ + @Override + public List<String> getTeamnamesForRepositoryRole(String role) { + List<String> list = new ArrayList<String>(); + try { + Properties allUsers = read(); + for (String team : allUsers.stringPropertyNames()) { + if (team.charAt(0) != '@') { + // skip users + continue; + } + String value = allUsers.getProperty(team); + String[] values = value.split(","); + for (int i = 0; i < values.length; i++) { + String r = values[i]; + if (r.equalsIgnoreCase(role)) { + // strip leading @ + list.add(team.substring(1)); + break; + } + } + } + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t); + } + Collections.sort(list); + return list; + } + + /** + * Sets the list of all teams who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @param teamnames + * @return true if successful + */ + @Override + public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) { + try { + Set<String> specifiedTeams = new HashSet<String>(teamnames); + Set<String> needsAddRole = new HashSet<String>(specifiedTeams); + Set<String> needsRemoveRole = new HashSet<String>(); + + // identify teams which require add and remove role + Properties allUsers = read(); + for (String team : allUsers.stringPropertyNames()) { + if (team.charAt(0) != '@') { + // skip users + continue; + } + String name = team.substring(1); + String value = allUsers.getProperty(team); + String[] values = value.split(","); + for (int i = 0; i < values.length; i++) { + String r = values[i]; + if (r.equalsIgnoreCase(role)) { + // team has role, check against revised team list + if (specifiedTeams.contains(name)) { + needsAddRole.remove(name); + } else { + // remove role from team + needsRemoveRole.add(name); + } + break; + } + } + } + + // add roles to teams + for (String name : needsAddRole) { + String team = "@" + name; + String teamValues = allUsers.getProperty(team); + teamValues += "," + role; + allUsers.put(team, teamValues); + } + + // remove role from team + for (String name : needsRemoveRole) { + String team = "@" + name; + String[] values = allUsers.getProperty(team).split(","); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + String value = values[i]; + if (!value.equalsIgnoreCase(role)) { + sb.append(value); + sb.append(','); + } + } + sb.setLength(sb.length() - 1); + + // update properties + allUsers.put(team, sb.toString()); + } + + // persist changes + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to set teamnames for role {0}!", role), t); + } + return false; + } + + /** + * Retrieve the team object for the specified team name. + * + * @param teamname + * @return a team object or null + * @since 0.8.0 + */ + @Override + public TeamModel getTeamModel(String teamname) { + read(); + TeamModel team = teams.get(teamname.toLowerCase()); + if (team != null) { + // clone the model, otherwise all changes to this object are + // live and unpersisted + team = DeepCopier.copy(team); + } + return team; + } + + /** + * Updates/writes a complete team object. + * + * @param model + * @return true if update is successful + * @since 0.8.0 + */ + @Override + public boolean updateTeamModel(TeamModel model) { + return updateTeamModel(model.name, model); + } + + /** + * Updates/writes all specified team objects. + * + * @param models a list of team models + * @return true if update is successful + * @since 1.2.0 + */ + public boolean updateTeamModels(Collection<TeamModel> models) { + try { + Properties allUsers = read(); + for (TeamModel model : models) { + updateTeamCache(allUsers, model.name, model); + } + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update {0} team models!", models.size()), t); + } + return false; + } + + /** + * Updates/writes and replaces a complete team object keyed by teamname. + * This method allows for renaming a team. + * + * @param teamname + * the old teamname + * @param model + * the team object to use for teamname + * @return true if update is successful + * @since 0.8.0 + */ + @Override + public boolean updateTeamModel(String teamname, TeamModel model) { + try { + Properties allUsers = read(); + updateTeamCache(allUsers, teamname, model); + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t); + } + return false; + } + + private void updateTeamCache(Properties allUsers, String teamname, TeamModel model) { + StringBuilder sb = new StringBuilder(); + List<String> roles; + if (model.permissions == null) { + // legacy, use repository list + if (model.repositories != null) { + roles = new ArrayList<String>(model.repositories); + } else { + roles = new ArrayList<String>(); + } + } else { + // discrete repository permissions + roles = new ArrayList<String>(); + for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) { + if (entry.getValue().exceeds(AccessPermission.NONE)) { + // code:repository (e.g. RW+:~james/myrepo.git + roles.add(entry.getValue().asRole(entry.getKey())); + } + } + } + + // Permissions + if (model.canAdmin) { + roles.add(Constants.ADMIN_ROLE); + } + if (model.canFork) { + roles.add(Constants.FORK_ROLE); + } + if (model.canCreate) { + roles.add(Constants.CREATE_ROLE); + } + + for (String role : roles) { + sb.append(role); + sb.append(','); + } + + if (!ArrayUtils.isEmpty(model.users)) { + for (String user : model.users) { + sb.append('!'); + sb.append(user); + sb.append(','); + } + } + if (!ArrayUtils.isEmpty(model.mailingLists)) { + for (String address : model.mailingLists) { + sb.append('&'); + sb.append(address); + sb.append(','); + } + } + if (!ArrayUtils.isEmpty(model.preReceiveScripts)) { + for (String script : model.preReceiveScripts) { + sb.append('^'); + sb.append(script); + sb.append(','); + } + } + if (!ArrayUtils.isEmpty(model.postReceiveScripts)) { + for (String script : model.postReceiveScripts) { + sb.append('%'); + sb.append(script); + sb.append(','); + } + } + // trim trailing comma + sb.setLength(sb.length() - 1); + allUsers.remove("@" + teamname); + allUsers.put("@" + model.name, sb.toString()); + + // update team cache + teams.remove(teamname.toLowerCase()); + teams.put(model.name.toLowerCase(), model); + } + + /** + * Deletes the team object from the user service. + * + * @param model + * @return true if successful + * @since 0.8.0 + */ + @Override + public boolean deleteTeamModel(TeamModel model) { + return deleteTeam(model.name); + } + + /** + * Delete the team object with the specified teamname + * + * @param teamname + * @return true if successful + * @since 0.8.0 + */ + @Override + public boolean deleteTeam(String teamname) { + Properties allUsers = read(); + teams.remove(teamname.toLowerCase()); + allUsers.remove("@" + teamname); + try { + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t); + } + return false; + } +} diff --git a/src/com/gitblit/GitblitUserService.java b/src/com/gitblit/GitblitUserService.java index 141ad8f1..543b8cc8 100644 --- a/src/com/gitblit/GitblitUserService.java +++ b/src/com/gitblit/GitblitUserService.java @@ -1,303 +1,304 @@ -/*
- * Copyright 2011 gitblit.com.
- *
- * 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.gitblit;
-
-import java.io.File;
-import java.io.IOException;
-import java.text.MessageFormat;
-import java.util.List;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.gitblit.models.TeamModel;
-import com.gitblit.models.UserModel;
-import com.gitblit.utils.DeepCopier;
-
-/**
- * This class wraps the default user service and is recommended as the starting
- * point for custom user service implementations.
- *
- * This does seem a little convoluted, but the idea is to allow IUserService to
- * evolve with new methods and implementations without breaking custom
- * authentication implementations.
- *
- * The most common implementation of a custom IUserService is to only override
- * authentication and then delegate all other functionality to one of Gitblit's
- * user services. This class optimizes that use-case.
- *
- * Extending GitblitUserService allows for authentication customization without
- * having to keep-up-with IUSerService API changes.
- *
- * @author James Moger
- *
- */
-public class GitblitUserService implements IUserService {
-
- protected IUserService serviceImpl;
-
- private final Logger logger = LoggerFactory.getLogger(GitblitUserService.class);
-
- public GitblitUserService() {
- }
-
- @Override
- public void setup(IStoredSettings settings) {
- File realmFile = GitBlit.getFileOrFolder(Keys.realm.userService, "users.conf");
- serviceImpl = createUserService(realmFile);
- logger.info("GUS delegating to " + serviceImpl.toString());
- }
-
- @SuppressWarnings("deprecation")
- protected IUserService createUserService(File realmFile) {
- IUserService service = null;
- if (realmFile.getName().toLowerCase().endsWith(".properties")) {
- // v0.5.0 - v0.7.0 properties-based realm file
- service = new FileUserService(realmFile);
- } else if (realmFile.getName().toLowerCase().endsWith(".conf")) {
- // v0.8.0+ config-based realm file
- service = new ConfigUserService(realmFile);
- }
-
- assert service != null;
-
- if (!realmFile.exists()) {
- // Create the Administrator account for a new realm file
- try {
- realmFile.createNewFile();
- } catch (IOException x) {
- logger.error(MessageFormat.format("COULD NOT CREATE REALM FILE {0}!", realmFile), x);
- }
- UserModel admin = new UserModel("admin");
- admin.password = "admin";
- admin.canAdmin = true;
- admin.excludeFromFederation = true;
- service.updateUserModel(admin);
- }
-
- if (service instanceof FileUserService) {
- // automatically create a users.conf realm file from the original
- // users.properties file
- File usersConfig = new File(realmFile.getParentFile(), "users.conf");
- if (!usersConfig.exists()) {
- logger.info(MessageFormat.format("Automatically creating {0} based on {1}",
- usersConfig.getAbsolutePath(), realmFile.getAbsolutePath()));
- ConfigUserService configService = new ConfigUserService(usersConfig);
- for (String username : service.getAllUsernames()) {
- UserModel userModel = service.getUserModel(username);
- configService.updateUserModel(userModel);
- }
- }
- // issue suggestion about switching to users.conf
- logger.warn("Please consider using \"users.conf\" instead of the deprecated \"users.properties\" file");
- }
- return service;
- }
-
- @Override
- public String toString() {
- return getClass().getSimpleName();
- }
-
- @Override
- public boolean supportsCredentialChanges() {
- return serviceImpl.supportsCredentialChanges();
- }
-
- @Override
- public boolean supportsDisplayNameChanges() {
- return serviceImpl.supportsDisplayNameChanges();
- }
-
- @Override
- public boolean supportsEmailAddressChanges() {
- return serviceImpl.supportsEmailAddressChanges();
- }
-
- @Override
- public boolean supportsTeamMembershipChanges() {
- return serviceImpl.supportsTeamMembershipChanges();
- }
-
- @Override
- public boolean supportsCookies() {
- return serviceImpl.supportsCookies();
- }
-
- @Override
- public String getCookie(UserModel model) {
- return serviceImpl.getCookie(model);
- }
-
- @Override
- public UserModel authenticate(char[] cookie) {
- return serviceImpl.authenticate(cookie);
- }
-
- @Override
- public UserModel authenticate(String username, char[] password) {
- return serviceImpl.authenticate(username, password);
- }
-
- @Override
- public void logout(UserModel user) {
- serviceImpl.logout(user);
- }
-
- @Override
- public UserModel getUserModel(String username) {
- return serviceImpl.getUserModel(username);
- }
-
- @Override
- public boolean updateUserModel(UserModel model) {
- return serviceImpl.updateUserModel(model);
- }
-
- @Override
- public boolean updateUserModels(List<UserModel> models) {
- return serviceImpl.updateUserModels(models);
- }
-
- @Override
- public boolean updateUserModel(String username, UserModel model) {
- if (supportsCredentialChanges()) {
- if (!supportsTeamMembershipChanges()) {
- // teams are externally controlled - copy from original model
- UserModel existingModel = getUserModel(username);
-
- model = DeepCopier.copy(model);
- model.teams.clear();
- model.teams.addAll(existingModel.teams);
- }
- return serviceImpl.updateUserModel(username, model);
- }
- if (model.username.equals(username)) {
- // passwords are not persisted by the backing user service
- model.password = null;
- if (!supportsTeamMembershipChanges()) {
- // teams are externally controlled- copy from original model
- UserModel existingModel = getUserModel(username);
-
- model = DeepCopier.copy(model);
- model.teams.clear();
- model.teams.addAll(existingModel.teams);
- }
- return serviceImpl.updateUserModel(username, model);
- }
- logger.error("Users can not be renamed!");
- return false;
- }
- @Override
- public boolean deleteUserModel(UserModel model) {
- return serviceImpl.deleteUserModel(model);
- }
-
- @Override
- public boolean deleteUser(String username) {
- return serviceImpl.deleteUser(username);
- }
-
- @Override
- public List<String> getAllUsernames() {
- return serviceImpl.getAllUsernames();
- }
-
- @Override
- public List<UserModel> getAllUsers() {
- return serviceImpl.getAllUsers();
- }
-
- @Override
- public List<String> getAllTeamNames() {
- return serviceImpl.getAllTeamNames();
- }
-
- @Override
- public List<TeamModel> getAllTeams() {
- return serviceImpl.getAllTeams();
- }
-
- @Override
- public List<String> getTeamnamesForRepositoryRole(String role) {
- return serviceImpl.getTeamnamesForRepositoryRole(role);
- }
-
- @Override
- @Deprecated
- public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) {
- return serviceImpl.setTeamnamesForRepositoryRole(role, teamnames);
- }
-
- @Override
- public TeamModel getTeamModel(String teamname) {
- return serviceImpl.getTeamModel(teamname);
- }
-
- @Override
- public boolean updateTeamModel(TeamModel model) {
- return serviceImpl.updateTeamModel(model);
- }
-
- @Override
- public boolean updateTeamModels(List<TeamModel> models) {
- return serviceImpl.updateTeamModels(models);
- }
-
- @Override
- public boolean updateTeamModel(String teamname, TeamModel model) {
- if (!supportsTeamMembershipChanges()) {
- // teams are externally controlled - copy from original model
- TeamModel existingModel = getTeamModel(teamname);
-
- model = DeepCopier.copy(model);
- model.users.clear();
- model.users.addAll(existingModel.users);
- }
- return serviceImpl.updateTeamModel(teamname, model);
- }
-
- @Override
- public boolean deleteTeamModel(TeamModel model) {
- return serviceImpl.deleteTeamModel(model);
- }
-
- @Override
- public boolean deleteTeam(String teamname) {
- return serviceImpl.deleteTeam(teamname);
- }
-
- @Override
- public List<String> getUsernamesForRepositoryRole(String role) {
- return serviceImpl.getUsernamesForRepositoryRole(role);
- }
-
- @Override
- @Deprecated
- public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
- return serviceImpl.setUsernamesForRepositoryRole(role, usernames);
- }
-
- @Override
- public boolean renameRepositoryRole(String oldRole, String newRole) {
- return serviceImpl.renameRepositoryRole(oldRole, newRole);
- }
-
- @Override
- public boolean deleteRepositoryRole(String role) {
- return serviceImpl.deleteRepositoryRole(role);
- }
-}
+/* + * Copyright 2011 gitblit.com. + * + * 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.gitblit; + +import java.io.File; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; +import com.gitblit.utils.DeepCopier; + +/** + * This class wraps the default user service and is recommended as the starting + * point for custom user service implementations. + * + * This does seem a little convoluted, but the idea is to allow IUserService to + * evolve with new methods and implementations without breaking custom + * authentication implementations. + * + * The most common implementation of a custom IUserService is to only override + * authentication and then delegate all other functionality to one of Gitblit's + * user services. This class optimizes that use-case. + * + * Extending GitblitUserService allows for authentication customization without + * having to keep-up-with IUSerService API changes. + * + * @author James Moger + * + */ +public class GitblitUserService implements IUserService { + + protected IUserService serviceImpl; + + private final Logger logger = LoggerFactory.getLogger(GitblitUserService.class); + + public GitblitUserService() { + } + + @Override + public void setup(IStoredSettings settings) { + File realmFile = GitBlit.getFileOrFolder(Keys.realm.userService, "users.conf"); + serviceImpl = createUserService(realmFile); + logger.info("GUS delegating to " + serviceImpl.toString()); + } + + @SuppressWarnings("deprecation") + protected IUserService createUserService(File realmFile) { + IUserService service = null; + if (realmFile.getName().toLowerCase().endsWith(".properties")) { + // v0.5.0 - v0.7.0 properties-based realm file + service = new FileUserService(realmFile); + } else if (realmFile.getName().toLowerCase().endsWith(".conf")) { + // v0.8.0+ config-based realm file + service = new ConfigUserService(realmFile); + } + + assert service != null; + + if (!realmFile.exists()) { + // Create the Administrator account for a new realm file + try { + realmFile.createNewFile(); + } catch (IOException x) { + logger.error(MessageFormat.format("COULD NOT CREATE REALM FILE {0}!", realmFile), x); + } + UserModel admin = new UserModel("admin"); + admin.password = "admin"; + admin.canAdmin = true; + admin.excludeFromFederation = true; + service.updateUserModel(admin); + } + + if (service instanceof FileUserService) { + // automatically create a users.conf realm file from the original + // users.properties file + File usersConfig = new File(realmFile.getParentFile(), "users.conf"); + if (!usersConfig.exists()) { + logger.info(MessageFormat.format("Automatically creating {0} based on {1}", + usersConfig.getAbsolutePath(), realmFile.getAbsolutePath())); + ConfigUserService configService = new ConfigUserService(usersConfig); + for (String username : service.getAllUsernames()) { + UserModel userModel = service.getUserModel(username); + configService.updateUserModel(userModel); + } + } + // issue suggestion about switching to users.conf + logger.warn("Please consider using \"users.conf\" instead of the deprecated \"users.properties\" file"); + } + return service; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + @Override + public boolean supportsCredentialChanges() { + return serviceImpl.supportsCredentialChanges(); + } + + @Override + public boolean supportsDisplayNameChanges() { + return serviceImpl.supportsDisplayNameChanges(); + } + + @Override + public boolean supportsEmailAddressChanges() { + return serviceImpl.supportsEmailAddressChanges(); + } + + @Override + public boolean supportsTeamMembershipChanges() { + return serviceImpl.supportsTeamMembershipChanges(); + } + + @Override + public boolean supportsCookies() { + return serviceImpl.supportsCookies(); + } + + @Override + public String getCookie(UserModel model) { + return serviceImpl.getCookie(model); + } + + @Override + public UserModel authenticate(char[] cookie) { + return serviceImpl.authenticate(cookie); + } + + @Override + public UserModel authenticate(String username, char[] password) { + return serviceImpl.authenticate(username, password); + } + + @Override + public void logout(UserModel user) { + serviceImpl.logout(user); + } + + @Override + public UserModel getUserModel(String username) { + return serviceImpl.getUserModel(username); + } + + @Override + public boolean updateUserModel(UserModel model) { + return serviceImpl.updateUserModel(model); + } + + @Override + public boolean updateUserModels(Collection<UserModel> models) { + return serviceImpl.updateUserModels(models); + } + + @Override + public boolean updateUserModel(String username, UserModel model) { + if (supportsCredentialChanges()) { + if (!supportsTeamMembershipChanges()) { + // teams are externally controlled - copy from original model + UserModel existingModel = getUserModel(username); + + model = DeepCopier.copy(model); + model.teams.clear(); + model.teams.addAll(existingModel.teams); + } + return serviceImpl.updateUserModel(username, model); + } + if (model.username.equals(username)) { + // passwords are not persisted by the backing user service + model.password = null; + if (!supportsTeamMembershipChanges()) { + // teams are externally controlled- copy from original model + UserModel existingModel = getUserModel(username); + + model = DeepCopier.copy(model); + model.teams.clear(); + model.teams.addAll(existingModel.teams); + } + return serviceImpl.updateUserModel(username, model); + } + logger.error("Users can not be renamed!"); + return false; + } + @Override + public boolean deleteUserModel(UserModel model) { + return serviceImpl.deleteUserModel(model); + } + + @Override + public boolean deleteUser(String username) { + return serviceImpl.deleteUser(username); + } + + @Override + public List<String> getAllUsernames() { + return serviceImpl.getAllUsernames(); + } + + @Override + public List<UserModel> getAllUsers() { + return serviceImpl.getAllUsers(); + } + + @Override + public List<String> getAllTeamNames() { + return serviceImpl.getAllTeamNames(); + } + + @Override + public List<TeamModel> getAllTeams() { + return serviceImpl.getAllTeams(); + } + + @Override + public List<String> getTeamnamesForRepositoryRole(String role) { + return serviceImpl.getTeamnamesForRepositoryRole(role); + } + + @Override + @Deprecated + public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) { + return serviceImpl.setTeamnamesForRepositoryRole(role, teamnames); + } + + @Override + public TeamModel getTeamModel(String teamname) { + return serviceImpl.getTeamModel(teamname); + } + + @Override + public boolean updateTeamModel(TeamModel model) { + return serviceImpl.updateTeamModel(model); + } + + @Override + public boolean updateTeamModels(Collection<TeamModel> models) { + return serviceImpl.updateTeamModels(models); + } + + @Override + public boolean updateTeamModel(String teamname, TeamModel model) { + if (!supportsTeamMembershipChanges()) { + // teams are externally controlled - copy from original model + TeamModel existingModel = getTeamModel(teamname); + + model = DeepCopier.copy(model); + model.users.clear(); + model.users.addAll(existingModel.users); + } + return serviceImpl.updateTeamModel(teamname, model); + } + + @Override + public boolean deleteTeamModel(TeamModel model) { + return serviceImpl.deleteTeamModel(model); + } + + @Override + public boolean deleteTeam(String teamname) { + return serviceImpl.deleteTeam(teamname); + } + + @Override + public List<String> getUsernamesForRepositoryRole(String role) { + return serviceImpl.getUsernamesForRepositoryRole(role); + } + + @Override + @Deprecated + public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) { + return serviceImpl.setUsernamesForRepositoryRole(role, usernames); + } + + @Override + public boolean renameRepositoryRole(String oldRole, String newRole) { + return serviceImpl.renameRepositoryRole(oldRole, newRole); + } + + @Override + public boolean deleteRepositoryRole(String role) { + return serviceImpl.deleteRepositoryRole(role); + } +} diff --git a/src/com/gitblit/IUserService.java b/src/com/gitblit/IUserService.java index 059d648a..6a3a3baf 100644 --- a/src/com/gitblit/IUserService.java +++ b/src/com/gitblit/IUserService.java @@ -1,324 +1,325 @@ -/*
- * Copyright 2011 gitblit.com.
- *
- * 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.gitblit;
-
-import java.util.List;
-
-import com.gitblit.models.TeamModel;
-import com.gitblit.models.UserModel;
-
-/**
- * Implementations of IUserService control all aspects of UserModel objects and
- * user authentication.
- *
- * @author James Moger
- *
- */
-public interface IUserService {
-
- /**
- * Setup the user service. This method allows custom implementations to
- * retrieve settings from gitblit.properties or the web.xml file without
- * relying on the GitBlit static singleton.
- *
- * @param settings
- * @since 0.7.0
- */
- void setup(IStoredSettings settings);
-
- /**
- * Does the user service support changes to credentials?
- *
- * @return true or false
- * @since 1.0.0
- */
- boolean supportsCredentialChanges();
-
- /**
- * Does the user service support changes to user display name?
- *
- * @return true or false
- * @since 1.0.0
- */
- boolean supportsDisplayNameChanges();
-
- /**
- * Does the user service support changes to user email address?
- *
- * @return true or false
- * @since 1.0.0
- */
- boolean supportsEmailAddressChanges();
-
- /**
- * Does the user service support changes to team memberships?
- *
- * @return true or false
- * @since 1.0.0
- */
- boolean supportsTeamMembershipChanges();
-
- /**
- * Does the user service support cookie authentication?
- *
- * @return true or false
- */
- boolean supportsCookies();
-
- /**
- * Returns the cookie value for the specified user.
- *
- * @param model
- * @return cookie value
- */
- String getCookie(UserModel model);
-
- /**
- * Authenticate a user based on their cookie.
- *
- * @param cookie
- * @return a user object or null
- */
- UserModel authenticate(char[] cookie);
-
- /**
- * Authenticate a user based on a username and password.
- *
- * @param username
- * @param password
- * @return a user object or null
- */
- UserModel authenticate(String username, char[] password);
-
- /**
- * Logout a user.
- *
- * @param user
- */
- void logout(UserModel user);
-
- /**
- * Retrieve the user object for the specified username.
- *
- * @param username
- * @return a user object or null
- */
- UserModel getUserModel(String username);
-
- /**
- * Updates/writes a complete user object.
- *
- * @param model
- * @return true if update is successful
- */
- boolean updateUserModel(UserModel model);
-
- /**
- * Updates/writes all specified user objects.
- *
- * @param models a list of user models
- * @return true if update is successful
- * @since 1.2.0
- */
- boolean updateUserModels(List<UserModel> models);
-
- /**
- * Adds/updates a user object keyed by username. This method allows for
- * renaming a user.
- *
- * @param username
- * the old username
- * @param model
- * the user object to use for username
- * @return true if update is successful
- */
- boolean updateUserModel(String username, UserModel model);
-
- /**
- * Deletes the user object from the user service.
- *
- * @param model
- * @return true if successful
- */
- boolean deleteUserModel(UserModel model);
-
- /**
- * Delete the user object with the specified username
- *
- * @param username
- * @return true if successful
- */
- boolean deleteUser(String username);
-
- /**
- * Returns the list of all users available to the login service.
- *
- * @return list of all usernames
- */
- List<String> getAllUsernames();
-
- /**
- * Returns the list of all users available to the login service.
- *
- * @return list of all users
- * @since 0.8.0
- */
- List<UserModel> getAllUsers();
-
- /**
- * Returns the list of all teams available to the login service.
- *
- * @return list of all teams
- * @since 0.8.0
- */
- List<String> getAllTeamNames();
-
- /**
- * Returns the list of all teams available to the login service.
- *
- * @return list of all teams
- * @since 0.8.0
- */
- List<TeamModel> getAllTeams();
-
- /**
- * Returns the list of all users who are allowed to bypass the access
- * restriction placed on the specified repository.
- *
- * @param role
- * the repository name
- * @return list of all usernames that can bypass the access restriction
- * @since 0.8.0
- */
- List<String> getTeamnamesForRepositoryRole(String role);
-
- /**
- * Sets the list of all teams who are allowed to bypass the access
- * restriction placed on the specified repository.
- *
- * @param role
- * the repository name
- * @param teamnames
- * @return true if successful
- * @since 0.8.0
- */
- @Deprecated
- boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames);
-
- /**
- * Retrieve the team object for the specified team name.
- *
- * @param teamname
- * @return a team object or null
- * @since 0.8.0
- */
- TeamModel getTeamModel(String teamname);
-
- /**
- * Updates/writes a complete team object.
- *
- * @param model
- * @return true if update is successful
- * @since 0.8.0
- */
- boolean updateTeamModel(TeamModel model);
-
- /**
- * Updates/writes all specified team objects.
- *
- * @param models a list of team models
- * @return true if update is successful
- * @since 1.2.0
- */
- boolean updateTeamModels(List<TeamModel> models);
-
- /**
- * Updates/writes and replaces a complete team object keyed by teamname.
- * This method allows for renaming a team.
- *
- * @param teamname
- * the old teamname
- * @param model
- * the team object to use for teamname
- * @return true if update is successful
- * @since 0.8.0
- */
- boolean updateTeamModel(String teamname, TeamModel model);
-
- /**
- * Deletes the team object from the user service.
- *
- * @param model
- * @return true if successful
- * @since 0.8.0
- */
- boolean deleteTeamModel(TeamModel model);
-
- /**
- * Delete the team object with the specified teamname
- *
- * @param teamname
- * @return true if successful
- * @since 0.8.0
- */
- boolean deleteTeam(String teamname);
-
- /**
- * Returns the list of all users who are allowed to bypass the access
- * restriction placed on the specified repository.
- *
- * @param role
- * the repository name
- * @return list of all usernames that can bypass the access restriction
- * @since 0.8.0
- */
- List<String> getUsernamesForRepositoryRole(String role);
-
- /**
- * Sets the list of all uses who are allowed to bypass the access
- * restriction placed on the specified repository.
- *
- * @param role
- * the repository name
- * @param usernames
- * @return true if successful
- */
- @Deprecated
- boolean setUsernamesForRepositoryRole(String role, List<String> usernames);
-
- /**
- * Renames a repository role.
- *
- * @param oldRole
- * @param newRole
- * @return true if successful
- */
- boolean renameRepositoryRole(String oldRole, String newRole);
-
- /**
- * Removes a repository role from all users.
- *
- * @param role
- * @return true if successful
- */
- boolean deleteRepositoryRole(String role);
-
- /**
- * @See java.lang.Object.toString();
- * @return string representation of the login service
- */
- String toString();
-}
+/* + * Copyright 2011 gitblit.com. + * + * 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.gitblit; + +import java.util.Collection; +import java.util.List; + +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; + +/** + * Implementations of IUserService control all aspects of UserModel objects and + * user authentication. + * + * @author James Moger + * + */ +public interface IUserService { + + /** + * Setup the user service. This method allows custom implementations to + * retrieve settings from gitblit.properties or the web.xml file without + * relying on the GitBlit static singleton. + * + * @param settings + * @since 0.7.0 + */ + void setup(IStoredSettings settings); + + /** + * Does the user service support changes to credentials? + * + * @return true or false + * @since 1.0.0 + */ + boolean supportsCredentialChanges(); + + /** + * Does the user service support changes to user display name? + * + * @return true or false + * @since 1.0.0 + */ + boolean supportsDisplayNameChanges(); + + /** + * Does the user service support changes to user email address? + * + * @return true or false + * @since 1.0.0 + */ + boolean supportsEmailAddressChanges(); + + /** + * Does the user service support changes to team memberships? + * + * @return true or false + * @since 1.0.0 + */ + boolean supportsTeamMembershipChanges(); + + /** + * Does the user service support cookie authentication? + * + * @return true or false + */ + boolean supportsCookies(); + + /** + * Returns the cookie value for the specified user. + * + * @param model + * @return cookie value + */ + String getCookie(UserModel model); + + /** + * Authenticate a user based on their cookie. + * + * @param cookie + * @return a user object or null + */ + UserModel authenticate(char[] cookie); + + /** + * Authenticate a user based on a username and password. + * + * @param username + * @param password + * @return a user object or null + */ + UserModel authenticate(String username, char[] password); + + /** + * Logout a user. + * + * @param user + */ + void logout(UserModel user); + + /** + * Retrieve the user object for the specified username. + * + * @param username + * @return a user object or null + */ + UserModel getUserModel(String username); + + /** + * Updates/writes a complete user object. + * + * @param model + * @return true if update is successful + */ + boolean updateUserModel(UserModel model); + + /** + * Updates/writes all specified user objects. + * + * @param models a list of user models + * @return true if update is successful + * @since 1.2.0 + */ + boolean updateUserModels(Collection<UserModel> models); + + /** + * Adds/updates a user object keyed by username. This method allows for + * renaming a user. + * + * @param username + * the old username + * @param model + * the user object to use for username + * @return true if update is successful + */ + boolean updateUserModel(String username, UserModel model); + + /** + * Deletes the user object from the user service. + * + * @param model + * @return true if successful + */ + boolean deleteUserModel(UserModel model); + + /** + * Delete the user object with the specified username + * + * @param username + * @return true if successful + */ + boolean deleteUser(String username); + + /** + * Returns the list of all users available to the login service. + * + * @return list of all usernames + */ + List<String> getAllUsernames(); + + /** + * Returns the list of all users available to the login service. + * + * @return list of all users + * @since 0.8.0 + */ + List<UserModel> getAllUsers(); + + /** + * Returns the list of all teams available to the login service. + * + * @return list of all teams + * @since 0.8.0 + */ + List<String> getAllTeamNames(); + + /** + * Returns the list of all teams available to the login service. + * + * @return list of all teams + * @since 0.8.0 + */ + List<TeamModel> getAllTeams(); + + /** + * Returns the list of all users who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @return list of all usernames that can bypass the access restriction + * @since 0.8.0 + */ + List<String> getTeamnamesForRepositoryRole(String role); + + /** + * Sets the list of all teams who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @param teamnames + * @return true if successful + * @since 0.8.0 + */ + @Deprecated + boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames); + + /** + * Retrieve the team object for the specified team name. + * + * @param teamname + * @return a team object or null + * @since 0.8.0 + */ + TeamModel getTeamModel(String teamname); + + /** + * Updates/writes a complete team object. + * + * @param model + * @return true if update is successful + * @since 0.8.0 + */ + boolean updateTeamModel(TeamModel model); + + /** + * Updates/writes all specified team objects. + * + * @param models a list of team models + * @return true if update is successful + * @since 1.2.0 + */ + boolean updateTeamModels(Collection<TeamModel> models); + + /** + * Updates/writes and replaces a complete team object keyed by teamname. + * This method allows for renaming a team. + * + * @param teamname + * the old teamname + * @param model + * the team object to use for teamname + * @return true if update is successful + * @since 0.8.0 + */ + boolean updateTeamModel(String teamname, TeamModel model); + + /** + * Deletes the team object from the user service. + * + * @param model + * @return true if successful + * @since 0.8.0 + */ + boolean deleteTeamModel(TeamModel model); + + /** + * Delete the team object with the specified teamname + * + * @param teamname + * @return true if successful + * @since 0.8.0 + */ + boolean deleteTeam(String teamname); + + /** + * Returns the list of all users who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @return list of all usernames that can bypass the access restriction + * @since 0.8.0 + */ + List<String> getUsernamesForRepositoryRole(String role); + + /** + * Sets the list of all uses who are allowed to bypass the access + * restriction placed on the specified repository. + * + * @param role + * the repository name + * @param usernames + * @return true if successful + */ + @Deprecated + boolean setUsernamesForRepositoryRole(String role, List<String> usernames); + + /** + * Renames a repository role. + * + * @param oldRole + * @param newRole + * @return true if successful + */ + boolean renameRepositoryRole(String oldRole, String newRole); + + /** + * Removes a repository role from all users. + * + * @param role + * @return true if successful + */ + boolean deleteRepositoryRole(String role); + + /** + * @See java.lang.Object.toString(); + * @return string representation of the login service + */ + String toString(); +} diff --git a/src/com/gitblit/LdapUserService.java b/src/com/gitblit/LdapUserService.java index 8da0ca35..b5d87a66 100644 --- a/src/com/gitblit/LdapUserService.java +++ b/src/com/gitblit/LdapUserService.java @@ -24,6 +24,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; import com.gitblit.utils.ArrayUtils; @@ -40,8 +43,6 @@ import com.unboundid.ldap.sdk.SearchScope; import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest; import com.unboundid.util.ssl.SSLUtil; import com.unboundid.util.ssl.TrustAllTrustManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Implementation of an LDAP user service. @@ -122,15 +123,17 @@ public class LdapUserService extends GitblitUserService { } } - for (UserModel user : ldapUsers.values()) { - // Push the ldap looked up values to backing file - super.updateUserModel(user); - if (!supportsTeamMembershipChanges()) { - for (TeamModel userTeam : user.teams) - updateTeamModel(userTeam); + super.updateUserModels(ldapUsers.values()); + + if (!supportsTeamMembershipChanges()) { + final Map<String, TeamModel> userTeams = new HashMap<String, TeamModel>(); + for (UserModel user : ldapUsers.values()) { + for (TeamModel userTeam : user.teams) { + userTeams.put(userTeam.name, userTeam); + } } + updateTeamModels(userTeams.values()); } - } } finally { ldapConnection.close(); |