Selaa lähdekoodia

Teams support.

Teams simplify the management of user-repository access permissions.  Teams have a list of restricted repositories.  Users are also added to teams and that grants them access to those repositories.

Federation and RPC support are still in-progress.
tags/v0.8.0
James Moger 12 vuotta sitten
vanhempi
commit
fe24a0be91

+ 1
- 0
docs/01_features.mkd Näytä tiedosto

@@ -13,6 +13,7 @@
- Gitweb inspired web UI
- Administrators may create, edit, rename, or delete repositories through the web UI or RPC interface
- Administrators may create, edit, rename, or delete users through the web UI or RPC interface
- Administrators may create, edit, rename, or delete teams through the web UI or RPC interface
- Repository Owners may edit repositories through the web UI
- Git-notes display support
- Branch metrics (uses Google Charts)

+ 32
- 147
docs/01_setup.mkd Näytä tiedosto

@@ -157,8 +157,13 @@ All repositories created with Gitblit are *bare* and will automatically have *.g
#### Repository Owner
The *Repository Owner* has the special permission of being able to edit a repository through the web UI. The Repository Owner is not permitted to rename the repository, delete the repository, or reassign ownership to another user.
### Administering Users (Gitblit v0.8.0+)
All users are stored in the `users.conf` file or in the file you specified in `gitblit.properties`.<br/>
### Teams
Since v0.8.0, Gitblit supports *teams* for the original `users.properties` user service and the current default user service `users.conf`. Teams have assigned users and assigned repositories. A user can be a member of multiple teams and a repository may belong to multiple teams. This allows the administrator to quickly add a user to a team without having to keep track of all the appropriate repositories.
### Administering Users (users.conf, Gitblit v0.8.0+)
All users are stored in the `users.conf` file or in the file you specified in `gitblit.properties`. Your file extension must be *.conf* in order to use this user service.
The `users.conf` file uses a Git-style configuration format:
[user "admin"]
@@ -167,14 +172,35 @@ The `users.conf` file uses a Git-style configuration format:
role = "#notfederated"
repository = repo1.git
repository = repo2.git
[user "hannibal"]
password = bossman
[user "faceman"]
password = vanity
[user "murdock"]
password = crazy
[user "babaracus"]
password = grrrr
[team "ateam"]
user = hannibal
user = faceman
user = murdock
user = babaracus
repository = topsecret.git
The `users.conf` file allows flexibility for adding new fields to a UserModel object that the original `users.properties` file does not afford without imposing the complexity of relying on an embedded SQL database.
### Administering Users (Gitblit v0.5.0 - v0.7.0)
All users are stored in the `users.properties` file or in the file you specified in `gitblit.properties`.<br/>
### Administering Users (users.properties, Gitblit v0.5.0 - v0.7.0)
All users are stored in the `users.properties` file or in the file you specified in `gitblit.properties`. Your file extension must be *.properties* in order to use this user service.
The format of `users.properties` follows Jetty's convention for HashRealms:
username,password,role1,role2,role3...
@teamname,!username1,!username2,!username3,repository1,repository2,repository3...
#### Usernames
Usernames must be unique and are case-insensitive.
@@ -191,149 +217,8 @@ Instead of maintaining a `users.conf` or `users.properties` file, you may want t
You may use your own custom *com.gitblit.IUserService* implementation by specifying its fully qualified classname in the *realm.userService* setting.
Your user service class must be on Gitblit's classpath and must have a public default constructor.
%BEGINCODE%
public interface IUserService {
/**
* Setup the user service.
*
* @param settings
* @since 0.7.0
*/
@Override
public void setup(IStoredSettings settings) {
}
/**
* 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
*/
char[] 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);
/**
* 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);
/**
* 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 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
*/
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
*/
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();
}
%ENDCODE%
Your user service class must be on Gitblit's classpath and must have a public default constructor.
Please see the following interface definition [com.gitblit.IUserService](https://github.com/gitblit/gitblit/blob/master/src/com/gitblit/IUserService.java).
## Client Setup and Configuration
### Https with Self-Signed Certificates

+ 1
- 0
docs/04_releases.mkd Näytä tiedosto

@@ -6,6 +6,7 @@
- added: new default user service implementation: com.gitblit.ConfigUserService (users.conf)
This user service implementation allows for serialization and deserialization of more sophisticated Gitblit User objects and will open the door for more advanced Gitblit features. For upgrading installations, a `users.conf` file will automatically be created for you from your existing `users.properties` file on your first launch of Gitblit. You will have to manually set *realm.userService=users.conf* to switch to the new user service. The original `users.properties` file and it's corresponding implementation are deprecated.
**New:** *realm.userService = users.conf*
- added: Teams for specifying user-repository access
- added: Gitblit Express bundle to get started running Gitblit on RedHat's OpenShift cloud
- added: optional Gravatar integration
**New:** *web.allowGravatar = true*

BIN
resources/users_16x16.png Näytä tiedosto


+ 289
- 18
src/com/gitblit/ConfigUserService.java Näytä tiedosto

@@ -32,7 +32,9 @@ import org.eclipse.jgit.util.FS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.DeepCopier;
import com.gitblit.utils.StringUtils;
/**
@@ -51,6 +53,16 @@ import com.gitblit.utils.StringUtils;
*/
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 REPOSITORY = "repository";
private static final String ROLE = "role";
private final File realmFile;
private final Logger logger = LoggerFactory.getLogger(ConfigUserService.class);
@@ -59,13 +71,7 @@ public class ConfigUserService implements IUserService {
private final Map<String, UserModel> cookies = new ConcurrentHashMap<String, UserModel>();
private final String userSection = "user";
private final String passwordField = "password";
private final String repositoryField = "repository";
private final String roleField = "role";
private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>();
private volatile long lastModified;
@@ -77,7 +83,7 @@ public class ConfigUserService implements IUserService {
* Setup the user service.
*
* @param settings
* @since 0.6.1
* @since 0.7.0
*/
@Override
public void setup(IStoredSettings settings) {
@@ -172,6 +178,11 @@ public class ConfigUserService implements IUserService {
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;
}
@@ -200,8 +211,34 @@ public class ConfigUserService implements IUserService {
public boolean updateUserModel(String username, UserModel model) {
try {
read();
users.remove(username.toLowerCase());
UserModel oldUser = 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 (oldUser != null) {
for (TeamModel team : oldUser.teams) {
if (!model.isTeamMember(team.name)) {
team.removeUser(username);
}
}
}
}
write();
return true;
} catch (Throwable t) {
@@ -233,7 +270,19 @@ public class ConfigUserService implements IUserService {
try {
// Read realm file
read();
users.remove(username.toLowerCase());
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) {
@@ -242,6 +291,172 @@ public class ConfigUserService implements IUserService {
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());
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.hasRepository(role)) {
list.add(model.name);
}
}
} catch (Throwable t) {
logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t);
}
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.addRepository(role);
} else {
// remove role from team
team.removeRepository(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 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 {
read();
teams.remove(teamname.toLowerCase());
teams.put(model.name.toLowerCase(), model);
write();
return true;
} catch (Throwable t) {
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.
*
@@ -337,6 +552,13 @@ public class ConfigUserService implements IUserService {
}
}
// identify teams which require role rename
for (TeamModel model : teams.values()) {
if (model.hasRepository(oldRole)) {
model.removeRepository(oldRole);
model.addRepository(newRole);
}
}
// persist changes
write();
return true;
@@ -363,6 +585,11 @@ public class ConfigUserService implements IUserService {
user.removeRepository(role);
}
// identify teams which require role rename
for (TeamModel team : teams.values()) {
team.removeRepository(role);
}
// persist changes
write();
return true;
@@ -383,8 +610,10 @@ public class ConfigUserService implements IUserService {
File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp");
StoredConfig config = new FileBasedConfig(realmFileCopy, FS.detect());
// write users
for (UserModel model : users.values()) {
config.setString(userSection, model.username, passwordField, model.password);
config.setString(USER, model.username, PASSWORD, model.password);
// user roles
List<String> roles = new ArrayList<String>();
@@ -394,12 +623,33 @@ public class ConfigUserService implements IUserService {
if (model.excludeFromFederation) {
roles.add(Constants.NOT_FEDERATED_ROLE);
}
config.setStringList(userSection, model.username, roleField, roles);
config.setStringList(USER, model.username, ROLE, roles);
// repository memberships
config.setStringList(userSection, model.username, repositoryField,
new ArrayList<String>(model.repositories));
// null check on "final" repositories because JSON-sourced UserModel
// can have a null repositories object
if (model.repositories != null) {
config.setStringList(USER, model.username, REPOSITORY, new ArrayList<String>(
model.repositories));
}
}
// write teams
for (TeamModel model : teams.values()) {
// null check on "final" repositories because JSON-sourced TeamModel
// can have a null repositories object
if (model.repositories != null) {
config.setStringList(TEAM, model.name, REPOSITORY, new ArrayList<String>(
model.repositories));
}
// null check on "final" users because JSON-sourced TeamModel
// can have a null users object
if (model.users != null) {
config.setStringList(TEAM, model.name, USER, new ArrayList<String>(model.users));
}
}
config.save();
// If the write is successful, delete the current file and rename
@@ -429,23 +679,25 @@ public class ConfigUserService implements IUserService {
lastModified = realmFile.lastModified();
users.clear();
cookies.clear();
teams.clear();
try {
StoredConfig config = new FileBasedConfig(realmFile, FS.detect());
config.load();
Set<String> usernames = config.getSubsections(userSection);
Set<String> usernames = config.getSubsections(USER);
for (String username : usernames) {
UserModel user = new UserModel(username);
user.password = config.getString(userSection, username, passwordField);
user.password = config.getString(USER, username, PASSWORD);
// user roles
Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList(
userSection, username, roleField)));
USER, username, ROLE)));
user.canAdmin = roles.contains(Constants.ADMIN_ROLE);
user.excludeFromFederation = roles.contains(Constants.NOT_FEDERATED_ROLE);
// repository memberships
Set<String> repositories = new HashSet<String>(Arrays.asList(config
.getStringList(userSection, username, repositoryField)));
.getStringList(USER, username, REPOSITORY)));
for (String repository : repositories) {
user.addRepository(repository);
}
@@ -454,6 +706,25 @@ public class ConfigUserService implements IUserService {
users.put(username, user);
cookies.put(StringUtils.getSHA1(username + user.password), user);
}
// load the teams
Set<String> teamnames = config.getSubsections(TEAM);
for (String teamname : teamnames) {
TeamModel team = new TeamModel(teamname);
team.addRepositories(Arrays.asList(config.getStringList(TEAM, teamname,
REPOSITORY)));
team.addUsers(Arrays.asList(config.getStringList(TEAM, teamname, USER)));
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);
}

+ 316
- 7
src/com/gitblit/FileUserService.java Näytä tiedosto

@@ -30,7 +30,9 @@ import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.DeepCopier;
import com.gitblit.utils.StringUtils;
/**
@@ -53,6 +55,8 @@ public class FileUserService extends FileSettings implements IUserService {
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());
}
@@ -61,7 +65,7 @@ public class FileUserService extends FileSettings implements IUserService {
* Setup the user service.
*
* @param settings
* @since 0.6.1
* @since 0.7.0
*/
@Override
public void setup(IStoredSettings settings) {
@@ -181,6 +185,12 @@ public class FileUserService extends FileSettings implements IUserService {
model.addRepository(role);
}
}
// set the teams for the user
for (TeamModel team : teams.values()) {
if (team.hasUser(username)) {
model.teams.add(DeepCopier.copy(team));
}
}
return model;
}
@@ -209,6 +219,7 @@ public class FileUserService extends FileSettings implements IUserService {
public boolean updateUserModel(String username, UserModel model) {
try {
Properties allUsers = read();
UserModel oldUser = getUserModel(username);
ArrayList<String> roles = new ArrayList<String>(model.repositories);
// Permissions
@@ -231,6 +242,32 @@ public class FileUserService extends FileSettings implements IUserService {
allUsers.remove(username);
allUsers.put(model.username, 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);
}
}
}
}
write(allUsers);
return true;
} catch (Throwable t) {
@@ -262,7 +299,17 @@ public class FileUserService extends FileSettings implements IUserService {
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) {
@@ -279,7 +326,14 @@ public class FileUserService extends FileSettings implements IUserService {
@Override
public List<String> getAllUsernames() {
Properties allUsers = read();
List<String> list = new ArrayList<String>(allUsers.stringPropertyNames());
List<String> list = new ArrayList<String>();
for (String user : allUsers.stringPropertyNames()) {
if (user.charAt(0) == '@') {
// skip team user definitions
continue;
}
list.add(user);
}
return list;
}
@@ -297,6 +351,9 @@ public class FileUserService extends FileSettings implements IUserService {
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)
@@ -315,7 +372,7 @@ public class FileUserService extends FileSettings implements IUserService {
}
/**
* Sets the list of all uses who are allowed to bypass the access
* Sets the list of all users who are allowed to bypass the access
* restriction placed on the specified repository.
*
* @param role
@@ -426,7 +483,7 @@ public class FileUserService extends FileSettings implements IUserService {
sb.append(',');
sb.append(newRole);
sb.append(',');
// skip first value (password)
for (int i = 1; i < values.length; i++) {
String value = values[i];
@@ -520,7 +577,7 @@ public class FileUserService extends FileSettings implements IUserService {
FileWriter writer = new FileWriter(realmFileCopy);
properties
.store(writer,
"# Gitblit realm file format: username=password,\\#permission,repository1,repository2...");
" 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.
@@ -551,11 +608,31 @@ public class FileUserService extends FileSettings implements IUserService {
if (lastRead != lastModified()) {
// reload hash cache
cookies.clear();
teams.clear();
for (String username : allUsers.stringPropertyNames()) {
String value = allUsers.getProperty(username);
String[] roles = value.split(",");
String password = roles[0];
cookies.put(StringUtils.getSHA1(username + password), username);
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>();
for (String role : roles) {
if (role.charAt(0) == '!') {
users.add(role.substring(1));
} else {
repositories.add(role);
}
}
team.addRepositories(repositories);
team.addUsers(users);
teams.put(team.name.toLowerCase(), team);
} else {
// user definition
String password = roles[0];
cookies.put(StringUtils.getSHA1(username + password), username);
}
}
}
return allUsers;
@@ -565,4 +642,236 @@ public class FileUserService extends FileSettings implements IUserService {
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());
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);
}
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 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();
for (String repository : model.repositories) {
sb.append(repository);
sb.append(',');
}
for (String user : model.users) {
sb.append('!');
sb.append(user);
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;
}
}

+ 80
- 1
src/com/gitblit/GitBlit.java Näytä tiedosto

@@ -69,6 +69,7 @@ import com.gitblit.models.RepositoryModel;
import com.gitblit.models.ServerSettings;
import com.gitblit.models.ServerStatus;
import com.gitblit.models.SettingModel;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ByteFormat;
import com.gitblit.utils.FederationUtils;
@@ -510,6 +511,83 @@ public class GitBlit implements ServletContextListener {
}
}
/**
* Returns the list of available teams that a user or repository may be
* assigned to.
*
* @return the list of teams
*/
public List<String> getAllTeamnames() {
List<String> teams = new ArrayList<String>(userService.getAllTeamNames());
Collections.sort(teams);
return teams;
}
/**
* Returns the TeamModel object for the specified name.
*
* @param teamname
* @return a TeamModel object or null
*/
public TeamModel getTeamModel(String teamname) {
return userService.getTeamModel(teamname);
}
/**
* Returns the list of all teams who are allowed to bypass the access
* restriction placed on the specified repository.
*
* @see IUserService.getTeamnamesForRepositoryRole(String)
* @param repository
* @return list of all teamnames that can bypass the access restriction
*/
public List<String> getRepositoryTeams(RepositoryModel repository) {
return userService.getTeamnamesForRepositoryRole(repository.name);
}
/**
* Sets the list of all uses who are allowed to bypass the access
* restriction placed on the specified repository.
*
* @see IUserService.setTeamnamesForRepositoryRole(String, List<String>)
* @param repository
* @param teamnames
* @return true if successful
*/
public boolean setRepositoryTeams(RepositoryModel repository, List<String> repositoryTeams) {
return userService.setTeamnamesForRepositoryRole(repository.name, repositoryTeams);
}
/**
* Updates the TeamModel object for the specified name.
*
* @param teamname
* @param team
* @param isCreate
*/
public void updateTeamModel(String teamname, TeamModel team, boolean isCreate) throws GitBlitException {
if (!teamname.equalsIgnoreCase(team.name)) {
if (userService.getTeamModel(team.name) != null) {
throw new GitBlitException(MessageFormat.format(
"Failed to rename ''{0}'' because ''{1}'' already exists.", teamname,
team.name));
}
}
if (!userService.updateTeamModel(teamname, team)) {
throw new GitBlitException(isCreate ? "Failed to add team!" : "Failed to update team!");
}
}
/**
* Delete the team object with the specified teamname
*
* @see IUserService.deleteTeam(String)
* @param teamname
* @return true if successful
*/
public boolean deleteTeam(String teamname) {
return userService.deleteTeam(teamname);
}
/**
* Clears all the cached data for the specified repository.
*
@@ -1115,6 +1193,7 @@ public class GitBlit implements ServletContextListener {
case PULL_REPOSITORIES:
return token.equals(all) || token.equals(unr) || token.equals(jur);
case PULL_USERS:
case PULL_TEAMS:
return token.equals(all) || token.equals(unr);
case PULL_SETTINGS:
return token.equals(all);
@@ -1473,7 +1552,7 @@ public class GitBlit implements ServletContextListener {
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")) {

+ 79
- 0
src/com/gitblit/IUserService.java Näytä tiedosto

@@ -17,6 +17,7 @@ package com.gitblit;
import java.util.List;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
/**
@@ -121,6 +122,84 @@ public interface IUserService {
*/
List<String> getAllUsernames();
/**
* 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 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
*/
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
*/
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 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.

+ 88
- 0
src/com/gitblit/models/TeamModel.java Näytä tiedosto

@@ -0,0 +1,88 @@
/*
* 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.models;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* TeamModel is a serializable model class that represents a group of users and
* a list of accessible repositories.
*
* @author James Moger
*
*/
public class TeamModel implements Serializable, Comparable<TeamModel> {
private static final long serialVersionUID = 1L;
// field names are reflectively mapped in EditTeam page
public String name;
public final Set<String> users = new HashSet<String>();
public final Set<String> repositories = new HashSet<String>();
public TeamModel(String name) {
this.name = name;
}
public boolean hasRepository(String name) {
return repositories.contains(name.toLowerCase());
}
public void addRepository(String name) {
repositories.add(name.toLowerCase());
}
public void addRepositories(Collection<String> names) {
for (String name:names) {
repositories.add(name.toLowerCase());
}
}
public void removeRepository(String name) {
repositories.remove(name.toLowerCase());
}
public boolean hasUser(String name) {
return users.contains(name.toLowerCase());
}
public void addUser(String name) {
users.add(name.toLowerCase());
}
public void addUsers(Collection<String> names) {
for (String name:names) {
users.add(name.toLowerCase());
}
}
public void removeUser(String name) {
users.remove(name.toLowerCase());
}
@Override
public String toString() {
return name;
}
@Override
public int compareTo(TeamModel o) {
return name.compareTo(o.name);
}
}

+ 23
- 2
src/com/gitblit/models/UserModel.java Näytä tiedosto

@@ -40,6 +40,7 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
public boolean canAdmin;
public boolean excludeFromFederation;
public final Set<String> repositories = new HashSet<String>();
public final Set<TeamModel> teams = new HashSet<TeamModel>();
public UserModel(String username) {
this.username = username;
@@ -54,13 +55,24 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
*/
@Deprecated
public boolean canAccessRepository(String repositoryName) {
return canAdmin || repositories.contains(repositoryName.toLowerCase());
return canAdmin || repositories.contains(repositoryName.toLowerCase())
|| hasTeamAccess(repositoryName);
}
public boolean canAccessRepository(RepositoryModel repository) {
boolean isOwner = !StringUtils.isEmpty(repository.owner)
&& repository.owner.equals(username);
return canAdmin || isOwner || repositories.contains(repository.name.toLowerCase());
return canAdmin || isOwner || repositories.contains(repository.name.toLowerCase())
|| hasTeamAccess(repository.name);
}
public boolean hasTeamAccess(String repositoryName) {
for (TeamModel team : teams) {
if (team.hasRepository(repositoryName)) {
return true;
}
}
return false;
}
public boolean hasRepository(String name) {
@@ -75,6 +87,15 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
repositories.remove(name.toLowerCase());
}
public boolean isTeamMember(String teamname) {
for (TeamModel team : teams) {
if (team.name.equalsIgnoreCase(teamname)) {
return true;
}
}
return false;
}
@Override
public String getName() {
return username;

+ 135
- 0
src/com/gitblit/utils/DeepCopier.java Näytä tiedosto

@@ -0,0 +1,135 @@
/*
* 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.utils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
public class DeepCopier {
/**
* Produce a deep copy of the given object. Serializes the entire object to
* a byte array in memory. Recommended for relatively small objects.
*/
@SuppressWarnings("unchecked")
public static <T> T copy(T original) {
T o = null;
try {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteOut);
oos.writeObject(original);
ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
ObjectInputStream ois = new ObjectInputStream(byteIn);
try {
o = (T) ois.readObject();
} catch (ClassNotFoundException cex) {
// actually can not happen in this instance
}
} catch (IOException iox) {
// doesn't seem likely to happen as these streams are in memory
throw new RuntimeException(iox);
}
return o;
}
/**
* This conserves heap memory!!!!! Produce a deep copy of the given object.
* Serializes the object through a pipe between two threads. Recommended for
* very large objects. The current thread is used for serializing the
* original object in order to respect any synchronization the caller may
* have around it, and a new thread is used for deserializing the copy.
*
*/
public static <T> T copyParallel(T original) {
try {
PipedOutputStream outputStream = new PipedOutputStream();
PipedInputStream inputStream = new PipedInputStream(outputStream);
ObjectOutputStream ois = new ObjectOutputStream(outputStream);
Receiver<T> receiver = new Receiver<T>(inputStream);
try {
ois.writeObject(original);
} finally {
ois.close();
}
return receiver.getResult();
} catch (IOException iox) {
// doesn't seem likely to happen as these streams are in memory
throw new RuntimeException(iox);
}
}
private static class Receiver<T> extends Thread {
private final InputStream inputStream;
private volatile T result;
private volatile Throwable throwable;
public Receiver(InputStream inputStream) {
this.inputStream = inputStream;
start();
}
@SuppressWarnings("unchecked")
public void run() {
try {
ObjectInputStream ois = new ObjectInputStream(inputStream);
try {
result = (T) ois.readObject();
try {
// Some serializers may write more than they actually
// need to deserialize the object, but if we don't
// read it all the PipedOutputStream will choke.
while (inputStream.read() != -1) {
}
} catch (IOException e) {
// The object has been successfully deserialized, so
// ignore problems at this point (for example, the
// serializer may have explicitly closed the inputStream
// itself, causing this read to fail).
}
} finally {
ois.close();
}
} catch (Throwable t) {
throwable = t;
}
}
public T getResult() throws IOException {
try {
join();
} catch (InterruptedException e) {
throw new RuntimeException("Unexpected InterruptedException", e);
}
// join() guarantees that all shared memory is synchronized between
// the two threads
if (throwable != null) {
if (throwable instanceof ClassNotFoundException) {
// actually can not happen in this instance
}
throw new RuntimeException(throwable);
}
return result;
}
}
}

+ 7
- 1
src/com/gitblit/wicket/GitBlitWebApp.properties Näytä tiedosto

@@ -187,4 +187,10 @@ gb.recentActivityNone = last {0} days / none
gb.dailyActivity = daily activity
gb.activeRepositories = active repositories
gb.activeAuthors = active authors
gb.commits = commits
gb.commits = commits
gb.teams = teams
gb.teamName = team name
gb.teamMembers = team members
gb.teamMemberships = team memberships
gb.newTeam = new team
gb.permittedTeams = permitted teams

+ 8
- 0
src/com/gitblit/wicket/WicketUtils.java Näytä tiedosto

@@ -259,6 +259,10 @@ public class WicketUtils {
return new PageParameters("user=" + username);
}
public static PageParameters newTeamnameParameter(String teamname) {
return new PageParameters("team=" + teamname);
}
public static PageParameters newRepositoryParameter(String repositoryName) {
return new PageParameters("r=" + repositoryName);
}
@@ -377,6 +381,10 @@ public class WicketUtils {
return params.getString("user", "");
}
public static String getTeamname(PageParameters params) {
return params.getString("team", "");
}
public static String getToken(PageParameters params) {
return params.getString("t", "");
}

+ 2
- 1
src/com/gitblit/wicket/pages/EditRepositoryPage.html Näytä tiedosto

@@ -7,7 +7,7 @@
<wicket:extend>
<body onload="document.getElementById('name').focus();">
<!-- Repository Table -->
<form wicket:id="editForm">
<form style="padding-top:5px;" wicket:id="editForm">
<table class="plain">
<tbody>
<tr><th><wicket:message key="gb.name"></wicket:message></th><td class="edit"><input class="span6" type="text" wicket:id="name" id="name" size="40" tabindex="1" /> &nbsp;<i><wicket:message key="gb.nameDescription"></wicket:message></i></td></tr>
@@ -24,6 +24,7 @@
<tr><td colspan="2"><hr></hr></td></tr>
<tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select class="span6" wicket:id="accessRestriction" tabindex="12" /></td></tr>
<tr><th style="vertical-align: top;"><wicket:message key="gb.permittedUsers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>
<tr><th style="vertical-align: top;"><wicket:message key="gb.permittedTeams"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>
<tr><td colspan="2"><hr></hr></td></tr>
<tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span6" wicket:id="federationStrategy" tabindex="13" /></td></tr>
<tr><th style="vertical-align: top;"><wicket:message key="gb.federationSets"></wicket:message></th><td style="padding:2px;"><span wicket:id="federationSets"></span></td></tr>

+ 18
- 1
src/com/gitblit/wicket/pages/EditRepositoryPage.java Näytä tiedosto

@@ -75,12 +75,14 @@ public class EditRepositoryPage extends RootSubPage {
List<String> federationSets = new ArrayList<String>();
List<String> repositoryUsers = new ArrayList<String>();
List<String> repositoryTeams = new ArrayList<String>();
if (isCreate) {
super.setupPage(getString("gb.newRepository"), "");
} else {
super.setupPage(getString("gb.edit"), repositoryModel.name);
if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
repositoryUsers.addAll(GitBlit.self().getRepositoryUsers(repositoryModel));
repositoryTeams.addAll(GitBlit.self().getRepositoryTeams(repositoryModel));
Collections.sort(repositoryUsers);
}
federationSets.addAll(repositoryModel.federationSets);
@@ -93,6 +95,11 @@ public class EditRepositoryPage extends RootSubPage {
repositoryUsers), new CollectionModel<String>(GitBlit.self().getAllUsernames()),
new ChoiceRenderer<String>("", ""), 10, false);
// teams palette
final Palette<String> teamsPalette = new Palette<String>("teams", new ListModel<String>(
repositoryTeams), new CollectionModel<String>(GitBlit.self().getAllTeamnames()),
new ChoiceRenderer<String>("", ""), 10, false);
// federation sets palette
List<String> sets = GitBlit.getStrings(Keys.federation.sets);
final Palette<String> federationSetsPalette = new Palette<String>("federationSets",
@@ -165,8 +172,9 @@ public class EditRepositoryPage extends RootSubPage {
// save the repository
GitBlit.self().updateRepositoryModel(oldName, repositoryModel, isCreate);
// save the repository access list
// repository access
if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
// save the user access list
Iterator<String> users = usersPalette.getSelectedChoices();
List<String> repositoryUsers = new ArrayList<String>();
while (users.hasNext()) {
@@ -178,6 +186,14 @@ public class EditRepositoryPage extends RootSubPage {
repositoryUsers.add(repositoryModel.owner);
}
GitBlit.self().setRepositoryUsers(repositoryModel, repositoryUsers);
// save the team access list
Iterator<String> teams = teamsPalette.getSelectedChoices();
List<String> repositoryTeams = new ArrayList<String>();
while (teams.hasNext()) {
repositoryTeams.add(teams.next());
}
GitBlit.self().setRepositoryTeams(repositoryModel, repositoryTeams);
}
} catch (GitBlitException e) {
error(e.getMessage());
@@ -215,6 +231,7 @@ public class EditRepositoryPage extends RootSubPage {
form.add(new CheckBox("skipSizeCalculation"));
form.add(new CheckBox("skipSummaryMetrics"));
form.add(usersPalette);
form.add(teamsPalette);
form.add(federationSetsPalette);
form.add(new Button("save"));

+ 24
- 0
src/com/gitblit/wicket/pages/EditTeamPage.html Näytä tiedosto

@@ -0,0 +1,24 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
xml:lang="en"
lang="en">
<wicket:extend>
<body onload="document.getElementById('name').focus();">
<!-- User Table -->
<form style="padding-top:5px;" wicket:id="editForm">
<table class="plain">
<tbody>
<tr><th><wicket:message key="gb.teamName"></wicket:message></th><td class="edit"><input type="text" wicket:id="name" id="name" size="30" tabindex="1" /></td></tr>
<tr><td colspan="2"><hr></hr></td></tr>
<tr><th style="vertical-align: top;"><wicket:message key="gb.teamMembers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>
<tr><td colspan="2"><hr></hr></td></tr>
<tr><th style="vertical-align: top;"><wicket:message key="gb.restrictedRepositories"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr>
<tr><th></th><td class="editButton"><input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="3" /> &nbsp; <input class="btn primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="4" /></td></tr>
</tbody>
</table>
</form>
</body>
</wicket:extend>
</html>

+ 169
- 0
src/com/gitblit/wicket/pages/EditTeamPage.java Näytä tiedosto

@@ -0,0 +1,169 @@
/*
* 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.wicket.pages;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.apache.wicket.PageParameters;
import org.apache.wicket.extensions.markup.html.form.palette.Palette;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.ChoiceRenderer;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.model.util.CollectionModel;
import org.apache.wicket.model.util.ListModel;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.GitBlit;
import com.gitblit.GitBlitException;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TeamModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.RequiresAdminRole;
import com.gitblit.wicket.WicketUtils;
@RequiresAdminRole
public class EditTeamPage extends RootSubPage {
private final boolean isCreate;
public EditTeamPage() {
// create constructor
super();
isCreate = true;
setupPage(new TeamModel(""));
}
public EditTeamPage(PageParameters params) {
// edit constructor
super(params);
isCreate = false;
String name = WicketUtils.getTeamname(params);
TeamModel model = GitBlit.self().getTeamModel(name);
setupPage(model);
}
protected void setupPage(final TeamModel teamModel) {
if (isCreate) {
super.setupPage(getString("gb.newTeam"), "");
} else {
super.setupPage(getString("gb.edit"), teamModel.name);
}
CompoundPropertyModel<TeamModel> model = new CompoundPropertyModel<TeamModel>(teamModel);
List<String> repos = new ArrayList<String>();
for (String repo : GitBlit.self().getRepositoryList()) {
RepositoryModel repositoryModel = GitBlit.self().getRepositoryModel(repo);
if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
repos.add(repo);
}
}
StringUtils.sortRepositorynames(repos);
List<String> teamUsers = new ArrayList<String>(teamModel.users);
Collections.sort(teamUsers);
final String oldName = teamModel.name;
final Palette<String> repositories = new Palette<String>("repositories",
new ListModel<String>(new ArrayList<String>(teamModel.repositories)),
new CollectionModel<String>(repos), new ChoiceRenderer<String>("", ""), 10, false);
final Palette<String> users = new Palette<String>("users", new ListModel<String>(
new ArrayList<String>(teamUsers)), new CollectionModel<String>(GitBlit.self()
.getAllUsernames()), new ChoiceRenderer<String>("", ""), 10, false);
Form<TeamModel> form = new Form<TeamModel>("editForm", model) {
private static final long serialVersionUID = 1L;
/*
* (non-Javadoc)
*
* @see org.apache.wicket.markup.html.form.Form#onSubmit()
*/
@Override
protected void onSubmit() {
String teamname = teamModel.name;
if (StringUtils.isEmpty(teamname)) {
error("Please enter a teamname!");
return;
}
if (isCreate) {
TeamModel model = GitBlit.self().getTeamModel(teamname);
if (model != null) {
error(MessageFormat.format("Team name ''{0}'' is unavailable.", teamname));
return;
}
}
Iterator<String> selectedRepositories = repositories.getSelectedChoices();
List<String> repos = new ArrayList<String>();
while (selectedRepositories.hasNext()) {
repos.add(selectedRepositories.next().toLowerCase());
}
teamModel.repositories.clear();
teamModel.repositories.addAll(repos);
Iterator<String> selectedUsers = users.getSelectedChoices();
List<String> members = new ArrayList<String>();
while (selectedUsers.hasNext()) {
members.add(selectedUsers.next().toLowerCase());
}
teamModel.users.clear();
teamModel.users.addAll(members);
try {
GitBlit.self().updateTeamModel(oldName, teamModel, isCreate);
} catch (GitBlitException e) {
error(e.getMessage());
return;
}
setRedirect(false);
if (isCreate) {
// create another team
info(MessageFormat.format("New team ''{0}'' successfully created.",
teamModel.name));
setResponsePage(EditTeamPage.class);
} else {
// back to users page
setResponsePage(UsersPage.class);
}
}
};
// field names reflective match TeamModel fields
form.add(new TextField<String>("name"));
form.add(repositories);
form.add(users);
form.add(new Button("save"));
Button cancel = new Button("cancel") {
private static final long serialVersionUID = 1L;
@Override
public void onSubmit() {
setResponsePage(UsersPage.class);
}
};
cancel.setDefaultFormProcessing(false);
form.add(cancel);
add(form);
}
}

+ 4
- 1
src/com/gitblit/wicket/pages/EditUserPage.html Näytä tiedosto

@@ -7,7 +7,7 @@
<wicket:extend>
<body onload="document.getElementById('username').focus();">
<!-- User Table -->
<form wicket:id="editForm">
<form style="padding-top:5px;" wicket:id="editForm">
<table class="plain">
<tbody>
<tr><th><wicket:message key="gb.username"></wicket:message></th><td class="edit"><input type="text" wicket:id="username" id="username" size="30" tabindex="1" /></td></tr>
@@ -15,6 +15,9 @@
<tr><th><wicket:message key="gb.confirmPassword"></wicket:message></th><td class="edit"><input type="password" wicket:id="confirmPassword" size="30" tabindex="3" /></td></tr>
<tr><th><wicket:message key="gb.canAdmin"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="canAdmin" tabindex="6" /> &nbsp;<i><wicket:message key="gb.canAdminDescription"></wicket:message></i></td></tr>
<tr><th><wicket:message key="gb.excludeFromFederation"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="excludeFromFederation" tabindex="7" /> &nbsp;<i><wicket:message key="gb.excludeFromFederationDescription"></wicket:message></i></td></tr>
<tr><td colspan="2"><hr></hr></td></tr>
<tr><th style="vertical-align: top;"><wicket:message key="gb.teamMemberships"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>
<tr><td colspan="2"><hr></hr></td></tr>
<tr><th style="vertical-align: top;"><wicket:message key="gb.restrictedRepositories"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr>
<tr><th></th><td class="editButton"><input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="8" /> &nbsp; <input class="btn primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="9" /></td></tr>
</tbody>

+ 25
- 1
src/com/gitblit/wicket/pages/EditUserPage.java Näytä tiedosto

@@ -17,6 +17,7 @@ package com.gitblit.wicket.pages;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -38,6 +39,7 @@ import com.gitblit.GitBlit;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.RequiresAdminRole;
@@ -82,10 +84,19 @@ public class EditUserPage extends RootSubPage {
repos.add(repo);
}
}
List<String> userTeams = new ArrayList<String>();
for (TeamModel team : userModel.teams) {
userTeams.add(team.name);
}
Collections.sort(userTeams);
final String oldName = userModel.username;
final Palette<String> repositories = new Palette<String>("repositories",
new ListModel<String>(new ArrayList<String>(userModel.repositories)),
new CollectionModel<String>(repos), new ChoiceRenderer<String>("", ""), 10, false);
final Palette<String> teams = new Palette<String>("teams", new ListModel<String>(
new ArrayList<String>(userTeams)), new CollectionModel<String>(GitBlit.self()
.getAllTeamnames()), new ChoiceRenderer<String>("", ""), 10, false);
Form<UserModel> form = new Form<UserModel>("editForm", model) {
private static final long serialVersionUID = 1L;
@@ -109,7 +120,8 @@ public class EditUserPage extends RootSubPage {
return;
}
}
boolean rename = !StringUtils.isEmpty(oldName) && !oldName.equalsIgnoreCase(username);
boolean rename = !StringUtils.isEmpty(oldName)
&& !oldName.equalsIgnoreCase(username);
if (!userModel.password.equals(confirmPassword.getObject())) {
error("Passwords do not match!");
return;
@@ -154,6 +166,17 @@ public class EditUserPage extends RootSubPage {
}
userModel.repositories.clear();
userModel.repositories.addAll(repos);
Iterator<String> selectedTeams = teams.getSelectedChoices();
userModel.teams.clear();
while (selectedTeams.hasNext()) {
TeamModel team = GitBlit.self().getTeamModel(selectedTeams.next());
if (team == null) {
continue;
}
userModel.teams.add(team);
}
try {
GitBlit.self().updateUserModel(oldName, userModel, isCreate);
} catch (GitBlitException e) {
@@ -185,6 +208,7 @@ public class EditUserPage extends RootSubPage {
form.add(new CheckBox("canAdmin"));
form.add(new CheckBox("excludeFromFederation"));
form.add(repositories);
form.add(teams);
form.add(new Button("save"));
Button cancel = new Button("cancel") {

+ 2
- 0
src/com/gitblit/wicket/pages/UsersPage.html Näytä tiedosto

@@ -5,6 +5,8 @@
lang="en">
<body>
<wicket:extend>
<div wicket:id="teamsPanel">[teams panel]</div>
<div wicket:id="usersPanel">[users panel]</div>
</wicket:extend>
</body>

+ 3
- 0
src/com/gitblit/wicket/pages/UsersPage.java Näytä tiedosto

@@ -16,6 +16,7 @@
package com.gitblit.wicket.pages;
import com.gitblit.wicket.RequiresAdminRole;
import com.gitblit.wicket.panels.TeamsPanel;
import com.gitblit.wicket.panels.UsersPanel;
@RequiresAdminRole
@@ -25,6 +26,8 @@ public class UsersPage extends RootPage {
super();
setupPage("", "");
add(new TeamsPanel("teamsPanel", showAdmin).setVisible(showAdmin));
add(new UsersPanel("usersPanel", showAdmin).setVisible(showAdmin));
}
}

+ 44
- 0
src/com/gitblit/wicket/panels/TeamsPanel.html Näytä tiedosto

@@ -0,0 +1,44 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
xml:lang="en"
lang="en">
<body>
<wicket:panel>
<div wicket:id="adminPanel">[admin links]</div>
<table class="repositories">
<tr>
<th class="left">
<img style="vertical-align: middle; border: 1px solid #888; background-color: white;" src="users_16x16.png"/>
<wicket:message key="gb.teams">[teams]</wicket:message>
</th>
<th class="right"></th>
</tr>
<tbody>
<tr wicket:id="teamRow">
<td class="left" ><div class="list" wicket:id="teamname">[teamname]</div></td>
<td class="rightAlign"><span wicket:id="teamLinks"></span></td>
</tr>
</tbody>
</table>
<wicket:fragment wicket:id="adminLinks">
<!-- page nav links -->
<div class="admin_nav">
<img style="vertical-align: middle;" src="add_16x16.png"/>
<a wicket:id="newTeam">
<wicket:message key="gb.newTeam"></wicket:message>
</a>
</div>
</wicket:fragment>
<wicket:fragment wicket:id="teamAdminLinks">
<span class="link"><a wicket:id="editTeam"><wicket:message key="gb.edit">[edit]</wicket:message></a> | <a wicket:id="deleteTeam"><wicket:message key="gb.delete">[delete]</wicket:message></a></span>
</wicket:fragment>
</wicket:panel>
</body>
</html>

+ 89
- 0
src/com/gitblit/wicket/panels/TeamsPanel.java Näytä tiedosto

@@ -0,0 +1,89 @@
/*
* 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.wicket.panels;
import java.text.MessageFormat;
import java.util.List;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import com.gitblit.GitBlit;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.EditTeamPage;
public class TeamsPanel extends BasePanel {
private static final long serialVersionUID = 1L;
public TeamsPanel(String wicketId, final boolean showAdmin) {
super(wicketId);
Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);
adminLinks.add(new BookmarkablePageLink<Void>("newTeam", EditTeamPage.class));
add(adminLinks.setVisible(showAdmin));
final List<String> teamnames = GitBlit.self().getAllTeamnames();
DataView<String> teamsView = new DataView<String>("teamRow", new ListDataProvider<String>(
teamnames)) {
private static final long serialVersionUID = 1L;
private int counter;
@Override
protected void onBeforeRender() {
super.onBeforeRender();
counter = 0;
}
public void populateItem(final Item<String> item) {
final String entry = item.getModelObject();
LinkPanel editLink = new LinkPanel("teamname", "list", entry, EditTeamPage.class,
WicketUtils.newTeamnameParameter(entry));
WicketUtils.setHtmlTooltip(editLink, getString("gb.edit") + " " + entry);
item.add(editLink);
Fragment teamLinks = new Fragment("teamLinks", "teamAdminLinks", this);
teamLinks.add(new BookmarkablePageLink<Void>("editTeam", EditTeamPage.class,
WicketUtils.newTeamnameParameter(entry)));
Link<Void> deleteLink = new Link<Void>("deleteTeam") {
private static final long serialVersionUID = 1L;
@Override
public void onClick() {
if (GitBlit.self().deleteTeam(entry)) {
teamnames.remove(entry);
info(MessageFormat.format("Team ''{0}'' deleted.", entry));
} else {
error(MessageFormat.format("Failed to delete team ''{0}''!", entry));
}
}
};
deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
"Delete team \"{0}\"?", entry)));
teamLinks.add(deleteLink);
item.add(teamLinks);
WicketUtils.setAlternatingBackground(item, counter);
counter++;
}
};
add(teamsView.setVisible(showAdmin));
}
}

+ 1
- 1
src/com/gitblit/wicket/panels/UsersPanel.html Näytä tiedosto

@@ -13,7 +13,7 @@
<tr>
<th class="left">
<img style="vertical-align: middle; border: 1px solid #888; background-color: white;" src="user_16x16.png"/>
<wicket:message key="gb.username">[username]</wicket:message>
<wicket:message key="gb.users">[users]</wicket:message>
</th>
<th class="right"></th>
</tr>

+ 107
- 0
tests/com/gitblit/tests/UserServiceTest.java Näytä tiedosto

@@ -38,6 +38,7 @@ public class UserServiceTest {
file.delete();
IUserService service = new FileUserService(file);
testUsers(service);
testTeams(service);
file.delete();
}
@@ -47,6 +48,7 @@ public class UserServiceTest {
file.delete();
IUserService service = new ConfigUserService(file);
testUsers(service);
testTeams(service);
file.delete();
}
@@ -106,4 +108,109 @@ public class UserServiceTest {
testUser = service.getUserModel("test");
assertTrue(testUser.hasRepository("newrepo1"));
}
protected void testTeams(IUserService service) {
// confirm we have no teams
assertEquals(0, service.getAllTeamNames().size());
// remove newrepo1 from test user
// now test user has no repositories
UserModel user = service.getUserModel("test");
user.repositories.clear();
service.updateUserModel(user);
user = service.getUserModel("test");
assertEquals(0, user.repositories.size());
assertFalse(user.canAccessRepository("newrepo1"));
assertFalse(user.canAccessRepository("NEWREPO1"));
// create test team and add test user and newrepo1
TeamModel team = new TeamModel("testteam");
team.addUser("test");
team.addRepository("newrepo1");
service.updateTeamModel(team);
// confirm 1 user and 1 repo
team = service.getTeamModel("testteam");
assertEquals(1, team.repositories.size());
assertEquals(1, team.users.size());
// confirm team membership
user = service.getUserModel("test");
assertEquals(0, user.repositories.size());
assertEquals(1, user.teams.size());
// confirm team access
assertTrue(team.hasRepository("newrepo1"));
assertTrue(user.hasTeamAccess("newrepo1"));
assertTrue(team.hasRepository("NEWREPO1"));
assertTrue(user.hasTeamAccess("NEWREPO1"));
// rename the team and add new repository
team.addRepository("newrepo2");
team.name = "testteam2";
service.updateTeamModel("testteam", team);
team = service.getTeamModel("testteam2");
user = service.getUserModel("test");
// confirm user and team can access newrepo2
assertEquals(2, team.repositories.size());
assertTrue(team.hasRepository("newrepo2"));
assertTrue(user.hasTeamAccess("newrepo2"));
assertTrue(team.hasRepository("NEWREPO2"));
assertTrue(user.hasTeamAccess("NEWREPO2"));
// delete testteam2
service.deleteTeam("testteam2");
team = service.getTeamModel("testteam2");
user = service.getUserModel("test");
// confirm team does not exist and user can not access newrepo1 and 2
assertEquals(null, team);
assertFalse(user.canAccessRepository("newrepo1"));
assertFalse(user.canAccessRepository("newrepo2"));
// create new team and add it to user
// this tests the inverse team creation/team addition
team = new TeamModel("testteam");
team.addRepository("NEWREPO1");
team.addRepository("NEWREPO2");
user.teams.add(team);
service.updateUserModel(user);
// confirm the inverted team addition
user = service.getUserModel("test");
team = service.getTeamModel("testteam");
assertTrue(user.hasTeamAccess("newrepo1"));
assertTrue(user.hasTeamAccess("newrepo2"));
assertTrue(team.hasUser("test"));
// drop testteam from user and add nextteam to user
team = new TeamModel("nextteam");
team.addRepository("NEWREPO1");
team.addRepository("NEWREPO2");
user.teams.clear();
user.teams.add(team);
service.updateUserModel(user);
// confirm implicit drop
user = service.getUserModel("test");
team = service.getTeamModel("testteam");
assertTrue(user.hasTeamAccess("newrepo1"));
assertTrue(user.hasTeamAccess("newrepo2"));
assertFalse(team.hasUser("test"));
team = service.getTeamModel("nextteam");
assertTrue(team.hasUser("test"));
// delete the user and confirm team no longer has user
service.deleteUser("test");
team = service.getTeamModel("testteam");
assertFalse(team.hasUser("test"));
// delete both teams
service.deleteTeam("testteam");
service.deleteTeam("nextteam");
assertEquals(0, service.getAllTeamNames().size());
}
}

Loading…
Peruuta
Tallenna