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
@@ -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) |
@@ -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 |
@@ -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* |
@@ -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); | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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")) { |
@@ -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. |
@@ -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); | |||
} | |||
} |
@@ -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; |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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 |
@@ -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", ""); | |||
} |
@@ -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" /> <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> |
@@ -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")); |
@@ -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" /> <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> |
@@ -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); | |||
} | |||
} |
@@ -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" /> <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" /> <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" /> <input class="btn primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="9" /></td></tr> | |||
</tbody> |
@@ -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") { |
@@ -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> |
@@ -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)); | |||
} | |||
} |
@@ -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> |
@@ -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)); | |||
} | |||
} |
@@ -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> |
@@ -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()); | |||
} | |||
} |