From 93f4729cdfc856d2a3b155bcf3e97f85b47ce760 Mon Sep 17 00:00:00 2001 From: James Moger Date: Sun, 4 Dec 2011 16:55:42 -0500 Subject: Implemented ConfigUserService. Fixed and deprecated FileUserService. --- src/com/gitblit/ConfigUserService.java | 471 ++++++++++++++++++++++++++++ src/com/gitblit/FederationPullExecutor.java | 4 +- src/com/gitblit/FileUserService.java | 27 +- src/com/gitblit/GitBlit.java | 38 ++- src/com/gitblit/models/UserModel.java | 8 + 5 files changed, 530 insertions(+), 18 deletions(-) create mode 100644 src/com/gitblit/ConfigUserService.java (limited to 'src/com') diff --git a/src/com/gitblit/ConfigUserService.java b/src/com/gitblit/ConfigUserService.java new file mode 100644 index 00000000..28a16c50 --- /dev/null +++ b/src/com/gitblit/ConfigUserService.java @@ -0,0 +1,471 @@ +/* + * 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.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.models.UserModel; +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 final File realmFile; + + private final Logger logger = LoggerFactory.getLogger(ConfigUserService.class); + + private final Map users = new ConcurrentHashMap(); + + private final Map cookies = new ConcurrentHashMap(); + + private final String userSection = "user"; + + private final String passwordField = "password"; + + private final String repositoryField = "repository"; + + private final String roleField = "role"; + + private volatile long lastModified; + + public ConfigUserService(File realmFile) { + this.realmFile = realmFile; + } + + /** + * Setup the user service. + * + * @param settings + * @since 0.6.1 + */ + @Override + public void setup(IStoredSettings settings) { + } + + /** + * 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 char[] getCookie(UserModel model) { + read(); + UserModel storedModel = users.get(model.username.toLowerCase()); + String cookie = StringUtils.getSHA1(model.username + storedModel.password); + return cookie.toCharArray(); + } + + /** + * 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; + } + + /** + * 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()); + 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 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 { + read(); + users.remove(username.toLowerCase()); + users.put(model.username.toLowerCase(), model); + write(); + 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 + read(); + users.remove(username.toLowerCase()); + write(); + 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 getAllUsernames() { + read(); + List list = new ArrayList(users.keySet()); + 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 getUsernamesForRepositoryRole(String role) { + List list = new ArrayList(); + try { + read(); + for (Map.Entry entry : users.entrySet()) { + UserModel model = entry.getValue(); + if (model.hasRepository(role)) { + list.add(model.username); + } + } + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t); + } + 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 + public boolean setUsernamesForRepositoryRole(String role, List usernames) { + try { + Set specifiedUsers = new HashSet(); + 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.addRepository(role); + } else { + // remove role from user + user.removeRepository(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.hasRepository(oldRole)) { + model.removeRepository(oldRole); + model.addRepository(newRole); + } + } + + // 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.removeRepository(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()); + for (UserModel model : users.values()) { + config.setString(userSection, model.username, passwordField, model.password); + + // user roles + List roles = new ArrayList(); + if (model.canAdmin) { + roles.add(Constants.ADMIN_ROLE); + } + if (model.excludeFromFederation) { + roles.add(Constants.NOT_FEDERATED_ROLE); + } + config.setStringList(userSection, model.username, roleField, roles); + + // repository memberships + config.setStringList(userSection, model.username, repositoryField, + new ArrayList(model.repositories)); + } + config.save(); + + // 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() && (realmFile.lastModified() > lastModified)) { + lastModified = realmFile.lastModified(); + users.clear(); + cookies.clear(); + try { + StoredConfig config = new FileBasedConfig(realmFile, FS.detect()); + config.load(); + Set usernames = config.getSubsections(userSection); + for (String username : usernames) { + UserModel user = new UserModel(username); + user.password = config.getString(userSection, username, passwordField); + + // user roles + Set roles = new HashSet(Arrays.asList(config.getStringList( + userSection, username, roleField))); + user.canAdmin = roles.contains(Constants.ADMIN_ROLE); + user.excludeFromFederation = roles.contains(Constants.NOT_FEDERATED_ROLE); + + // repository memberships + Set repositories = new HashSet(Arrays.asList(config + .getStringList(userSection, username, repositoryField))); + for (String repository : repositories) { + user.addRepository(repository); + } + + // update cache + users.put(username, user); + cookies.put(StringUtils.getSHA1(username + user.password), user); + } + } 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/FederationPullExecutor.java b/src/com/gitblit/FederationPullExecutor.java index b190e7b5..20fd67c6 100644 --- a/src/com/gitblit/FederationPullExecutor.java +++ b/src/com/gitblit/FederationPullExecutor.java @@ -285,9 +285,9 @@ public class FederationPullExecutor implements Runnable { Collection users = FederationUtils.getUsers(registration); if (users != null && users.size() > 0) { File realmFile = new File(registrationFolderFile, registration.name - + "_users.properties"); + + "_users.conf"); realmFile.delete(); - FileUserService userService = new FileUserService(realmFile); + ConfigUserService userService = new ConfigUserService(realmFile); for (UserModel user : users) { userService.updateUserModel(user.username, user); diff --git a/src/com/gitblit/FileUserService.java b/src/com/gitblit/FileUserService.java index 3c8914dd..a98e4175 100644 --- a/src/com/gitblit/FileUserService.java +++ b/src/com/gitblit/FileUserService.java @@ -34,14 +34,19 @@ import com.gitblit.models.UserModel; import com.gitblit.utils.StringUtils; /** - * FileUserService is Gitblit's default user service implementation. + * 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); @@ -360,12 +365,11 @@ public class FileUserService extends FileSettings implements IUserService { StringBuilder sb = new StringBuilder(); sb.append(password); sb.append(','); - List revisedRoles = new ArrayList(); + // skip first value (password) for (int i = 1; i < values.length; i++) { String value = values[i]; if (!value.equalsIgnoreCase(role)) { - revisedRoles.add(value); sb.append(value); sb.append(','); } @@ -406,7 +410,7 @@ public class FileUserService extends FileSettings implements IUserService { for (int i = 1; i < roles.length; i++) { String r = roles[i]; if (r.equalsIgnoreCase(oldRole)) { - needsRenameRole.remove(username); + needsRenameRole.add(username); break; } } @@ -420,13 +424,13 @@ public class FileUserService extends FileSettings implements IUserService { StringBuilder sb = new StringBuilder(); sb.append(password); sb.append(','); - List revisedRoles = new ArrayList(); - revisedRoles.add(newRole); + sb.append(newRole); + sb.append(','); + // skip first value (password) for (int i = 1; i < values.length; i++) { String value = values[i]; if (!value.equalsIgnoreCase(oldRole)) { - revisedRoles.add(value); sb.append(value); sb.append(','); } @@ -467,7 +471,7 @@ public class FileUserService extends FileSettings implements IUserService { for (int i = 1; i < roles.length; i++) { String r = roles[i]; if (r.equalsIgnoreCase(role)) { - needsDeleteRole.remove(username); + needsDeleteRole.add(username); break; } } @@ -481,12 +485,10 @@ public class FileUserService extends FileSettings implements IUserService { StringBuilder sb = new StringBuilder(); sb.append(password); sb.append(','); - List revisedRoles = new ArrayList(); // skip first value (password) for (int i = 1; i < values.length; i++) { String value = values[i]; if (!value.equalsIgnoreCase(role)) { - revisedRoles.add(value); sb.append(value); sb.append(','); } @@ -558,4 +560,9 @@ public class FileUserService extends FileSettings implements IUserService { } return allUsers; } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + propertiesFile.getAbsolutePath() + ")"; + } } diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index 80550f49..60a96e66 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -1435,6 +1435,7 @@ public class GitBlit implements ServletContextListener { * * @param settings */ + @SuppressWarnings("deprecation") public void configureContext(IStoredSettings settings, boolean startFederation) { logger.info("Reading configuration from " + settings.toString()); this.settings = settings; @@ -1453,20 +1454,45 @@ public class GitBlit implements ServletContextListener { } catch (Throwable t) { // not a login service class or class could not be instantiated. // try to use default file login service - File realmFile = getFileOrFolder(Keys.realm.userService, "users.properties"); + File realmFile = getFileOrFolder(Keys.realm.userService, "users.conf"); if (realmFile.exists()) { // load the existing realm file - loginService = new FileUserService(realmFile); + if (realmFile.getName().toLowerCase().endsWith(".properties")) { + // load the v0.5.0 - v0.7.0 properties-based realm file + loginService = new FileUserService(realmFile); + + // 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 : loginService.getAllUsernames()) { + UserModel userModel = loginService.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"); + } else if (realmFile.getName().toLowerCase().endsWith(".conf")) { + // load the config-based realm file + loginService = new ConfigUserService(realmFile); + } } else { - // create a new realm file and add the default admin account. - // this is necessary for bootstrapping a dynamic environment - // like running on a cloud service. + // Create a new realm file and add the default admin + // account. This is necessary for bootstrapping a dynamic + // environment like running on a cloud service. + // As of v0.8.0 the default realm file is ConfigUserService. try { + realmFile = getFileOrFolder(Keys.realm.userService, "users.conf"); realmFile.createNewFile(); - loginService = new FileUserService(realmFile); + loginService = new ConfigUserService(realmFile); UserModel admin = new UserModel("admin"); admin.password = "admin"; admin.canAdmin = true; + admin.excludeFromFederation = true; loginService.updateUserModel(admin); } catch (IOException x) { logger.error( diff --git a/src/com/gitblit/models/UserModel.java b/src/com/gitblit/models/UserModel.java index dadc44e7..8c99512b 100644 --- a/src/com/gitblit/models/UserModel.java +++ b/src/com/gitblit/models/UserModel.java @@ -63,10 +63,18 @@ public class UserModel implements Principal, Serializable, Comparable return canAdmin || isOwner || repositories.contains(repository.name.toLowerCase()); } + public boolean hasRepository(String name) { + return repositories.contains(name.toLowerCase()); + } + public void addRepository(String name) { repositories.add(name.toLowerCase()); } + public void removeRepository(String name) { + repositories.remove(name.toLowerCase()); + } + @Override public String getName() { return username; -- cgit v1.2.3