From dfb88962fdbd29f59abe92178bb042738d57c3e1 Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Wed, 11 May 2011 22:07:18 -0400 Subject: [PATCH] Add user. Implemented repository view and git access restrictions. --- src/com/gitblit/ILoginService.java | 9 src/com/gitblit/wicket/pages/RepositoriesPage.java | 42 ++- src/com/gitblit/wicket/resources/lock_go_16x16.png | 0 src/com/gitblit/wicket/pages/EditUserPage.html | 29 ++ gitblit.properties | 14 src/com/gitblit/GitBlit.java | 45 ++ src/com/gitblit/wicket/GitBlitWebApp.properties | 14 src/com/gitblit/wicket/WicketUtils.java | 4 src/com/gitblit/GitBlitServlet.java | 84 ++++++ src/com/gitblit/GitBlitServer.java | 57 +-- src/com/gitblit/wicket/pages/RepositoriesPage.html | 2 src/com/gitblit/wicket/pages/EditRepositoryPage.html | 16 users.properties | 5 src/com/gitblit/Constants.java | 37 ++ src/com/gitblit/wicket/pages/EditRepositoryPage.java | 9 src/com/gitblit/wicket/AuthorizationStrategy.java | 1 src/com/gitblit/wicket/RepositoryPage.java | 10 src/com/gitblit/wicket/GitBlitWebSession.java | 2 src/com/gitblit/wicket/LoginPage.java | 1 src/com/gitblit/wicket/models/RepositoryModel.java | 11 src/com/gitblit/wicket/pages/EditUserPage.java | 97 ++++++ src/com/gitblit/wicket/resources/shield_16x16.png | 0 src/com/gitblit/wicket/resources/gitblit.css | 28 + src/com/gitblit/wicket/models/User.java | 89 ++++++ src/com/gitblit/wicket/resources/lock_pull_16x16.png | 0 /dev/null | 54 --- src/com/gitblit/JettyLoginService.java | 180 ++++++++++++ 27 files changed, 679 insertions(+), 161 deletions(-) diff --git a/gitblit.properties b/gitblit.properties index c51fffd..2bdcf2c 100644 --- a/gitblit.properties +++ b/gitblit.properties @@ -3,7 +3,7 @@ # # Allow push/pull over http/https with JGit servlet -git.allowPushPull = true +git.enableGitServlet = true # Base folder for repositories # Use forward slashes even on Windows!! @@ -23,9 +23,6 @@ # # Authentication Settings # - -# Require authentication for http/https push/pull access of git repositories -git.authenticate = true # Require authentication to see everything but the admin pages web.authenticateViewPages = false @@ -131,11 +128,11 @@ # Jetty Settings # -# use NIO connectors. If false, socket connectors will be used. +# Use Jetty NIO connectors. If false, Jetty Socket connectors will be used. server.useNio = true # Standard http port to serve. <= 0 disables this connector. -server.httpPort = 80 +server.httpPort = 0 # Secure/SSL https port to serve. <= 0 disables this connector. server.httpsPort = 443 @@ -148,7 +145,10 @@ # You may specify an ip or an empty value to bind to all interfaces. server.httpsBindInterface = localhost -# Password for SSL keystore (keystore password and certificate password must match) +# Password for SSL keystore. +# Keystore password and certificate password must match. +# This is provided for convenience, its probably more secure to set this value +# using the --storePassword command line parameter. server.storePassword = dosomegit # Port for shutdown monitor to listen on. diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java index 38f2e7d..3ca917d 100644 --- a/src/com/gitblit/Constants.java +++ b/src/com/gitblit/Constants.java @@ -6,14 +6,41 @@ public final static String VERSION = "0.1.0-SNAPSHOT"; - public final static String ADMIN_ROLE = "admin"; - - public final static String PULL_ROLE = "pull"; - - public final static String PUSH_ROLE = "push"; + public final static String ADMIN_ROLE = "#admin"; public final static String PROPERTIES_FILE = "gitblit.properties"; + public static enum AccessRestrictionType { + NONE, PUSH, CLONE, VIEW; + + public static AccessRestrictionType fromString(String name) { + for (AccessRestrictionType type : values()) { + if (type.toString().equalsIgnoreCase(name)) { + return type; + } + } + return NONE; + } + + public boolean atLeast(AccessRestrictionType type) { + return this.ordinal() >= type.ordinal(); + } + + public String toString() { + switch (this) { + case NONE: + return "none"; + case PUSH: + return "push"; + case CLONE: + return "clone"; + case VIEW: + return "view"; + } + return "none"; + } + } + public static String getGitBlitVersion() { return NAME + " v" + VERSION; } diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index c633f6e..bdfa590 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -19,9 +19,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.Constants.AccessRestrictionType; import com.gitblit.utils.JGitUtils; -import com.gitblit.wicket.User; import com.gitblit.wicket.models.RepositoryModel; +import com.gitblit.wicket.models.User; public class GitBlit implements ServletContextListener { @@ -94,7 +95,18 @@ userCookie.setPath("/"); response.addCookie(userCookie); } - + + public User getUser(String username) { + User user = loginService.getUserModel(username); + return user; + } + + public void editUserModel(User user, boolean isCreate) throws GitBlitException { + if (!loginService.updateUserModel(user)) { + throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!"); + } + } + public List<String> getRepositoryList() { return JGitUtils.getRepositoryList(repositoriesFolder, exportAll, storedSettings.getBoolean(Keys.git.nestedRepositories, true)); } @@ -112,17 +124,31 @@ } return r; } - - public List<RepositoryModel> getRepositoryModels() { + + public List<RepositoryModel> getRepositoryModels(User user) { List<String> list = getRepositoryList(); List<RepositoryModel> repositories = new ArrayList<RepositoryModel>(); for (String repo : list) { - RepositoryModel model = getRepositoryModel(repo); - repositories.add(model); + RepositoryModel model = getRepositoryModel(user, repo); + if (model != null) { + repositories.add(model); + } } return repositories; } + public RepositoryModel getRepositoryModel(User user, String repositoryName) { + RepositoryModel model = getRepositoryModel(repositoryName); + if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) { + if (user != null && user.canView(model)) { + return model; + } + return null; + } else { + return model; + } + } + public RepositoryModel getRepositoryModel(String repositoryName) { Repository r = getRepository(repositoryName); RepositoryModel model = new RepositoryModel(); @@ -133,10 +159,9 @@ if (config != null) { model.description = config.getString("gitblit", null, "description"); model.owner = config.getString("gitblit", null, "owner"); - model.group = config.getString("gitblit", null, "group"); model.useTickets = config.getBoolean("gitblit", "useTickets", false); model.useDocs = config.getBoolean("gitblit", "useDocs", false); - model.useRestrictedAccess = config.getBoolean("gitblit", "restrictedAccess", false); + model.accessRestriction = AccessRestrictionType.fromString(config.getString("gitblit", null, "accessRestriction")); model.showRemoteBranches = config.getBoolean("gitblit", "showRemoteBranches", false); } r.close(); @@ -149,7 +174,7 @@ if (new File(repositoriesFolder, repository.name).exists()) { throw new GitBlitException(MessageFormat.format("Can not create repository {0} because it already exists.", repository.name)); } - // create repository + // create repository logger.info("create repository " + repository.name); r = JGitUtils.createRepository(repositoriesFolder, repository.name, true); } else { @@ -170,7 +195,7 @@ config.setString("gitblit", null, "owner", repository.owner); config.setBoolean("gitblit", null, "useTickets", repository.useTickets); config.setBoolean("gitblit", null, "useDocs", repository.useDocs); - config.setBoolean("gitblit", null, "restrictedAccess", repository.useRestrictedAccess); + config.setString("gitblit", null, "accessRestriction", repository.accessRestriction.toString()); config.setBoolean("gitblit", null, "showRemoteBranches", repository.showRemoteBranches); try { config.save(); diff --git a/src/com/gitblit/GitBlitServer.java b/src/com/gitblit/GitBlitServer.java index 0978bc8..f5ed91a 100644 --- a/src/com/gitblit/GitBlitServer.java +++ b/src/com/gitblit/GitBlitServer.java @@ -56,7 +56,6 @@ import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.webapp.WebAppContext; -import org.eclipse.jgit.http.server.GitServlet; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; @@ -222,44 +221,42 @@ // Git Servlet ServletHolder gitServlet = null; String gitServletPathSpec = "/git/*"; - if (fileSettings.getBoolean(Keys.git.allowPushPull, true)) { - gitServlet = rootContext.addServlet(GitServlet.class, gitServletPathSpec); + if (fileSettings.getBoolean(Keys.git.enableGitServlet, true)) { + gitServlet = rootContext.addServlet(GitBlitServlet.class, gitServletPathSpec); gitServlet.setInitParameter("base-path", params.repositoriesFolder); - gitServlet.setInitParameter("export-all", params.exportAll ? "1" : "0"); + gitServlet.setInitParameter("export-all", fileSettings.getBoolean(Keys.git.exportAll, true) ? "1" : "0"); } // Login Service LoginService loginService = null; - String realmUsers = params.realmFile; - if (realmUsers != null && new File(realmUsers).exists()) { - logger.info("Setting up login service from " + realmUsers); - JettyLoginService jettyLoginService = new JettyLoginService(realmUsers); - GitBlit.self().setLoginService(jettyLoginService); - loginService = jettyLoginService; + String realmUsers = params.realmFile; + if (!StringUtils.isEmpty(realmUsers)) { + File realmFile = new File(realmUsers); + if (realmFile.exists()) { + logger.info("Setting up login service from " + realmUsers); + JettyLoginService jettyLoginService = new JettyLoginService(realmFile); + GitBlit.self().setLoginService(jettyLoginService); + loginService = jettyLoginService; + } } // Determine what handler to use Handler handler; if (gitServlet != null) { - if (loginService != null && params.authenticatePushPull) { - // Authenticate Pull/Push - String[] roles = new String[] { Constants.PULL_ROLE, Constants.PUSH_ROLE }; - logger.info("Authentication required for git servlet pull/push access"); + if (loginService != null) { + // Authenticate Clone/Push + logger.info("Setting up authenticated git servlet clone/push access"); Constraint constraint = new Constraint(); - constraint.setName("auth"); constraint.setAuthenticate(true); - constraint.setRoles(roles); + constraint.setRoles(new String [] { "*" }); ConstraintMapping mapping = new ConstraintMapping(); mapping.setPathSpec(gitServletPathSpec); mapping.setConstraint(constraint); - ConstraintSecurityHandler security = new ConstraintSecurityHandler(); + ConstraintSecurityHandler security = new ConstraintSecurityHandler(); security.addConstraintMapping(mapping); - for (String role : roles) { - security.addRole(role); - } security.setAuthenticator(new BasicAuthenticator()); security.setLoginService(loginService); security.setStrict(false); @@ -273,7 +270,7 @@ handler = rootContext; } } else { - logger.info("Git servlet pull/push disabled"); + logger.info("Git servlet clone/push disabled"); handler = rootContext; } @@ -448,37 +445,31 @@ @Parameter(names = { "--stop" }, description = "Stop Server") public Boolean stop = false; - @Parameter(names = { "--temp" }, description = "Server temp folder") + @Parameter(names = { "--tempFolder" }, description = "Server temp folder") public String temp = fileSettings.getString(Keys.server.tempFolder, "temp"); /* * GIT Servlet Parameters */ - @Parameter(names = { "--repos" }, description = "Git Repositories Folder") + @Parameter(names = { "--repositoriesFolder" }, description = "Git Repositories Folder") public String repositoriesFolder = fileSettings.getString(Keys.git.repositoriesFolder, "repos"); - - @Parameter(names = { "--exportAll" }, description = "Export All Found Repositories") - public Boolean exportAll = fileSettings.getBoolean(Keys.git.exportAll, true); /* * Authentication Parameters */ - @Parameter(names = { "--authenticatePushPull" }, description = "Authenticate Git Push/Pull access") - public Boolean authenticatePushPull = fileSettings.getBoolean(Keys.git.authenticate, true); - - @Parameter(names = { "--realm" }, description = "Users Realm Hash File") + @Parameter(names = { "--realmFile" }, description = "Users Realm Hash File") public String realmFile = fileSettings.getString(Keys.server.realmFile, "users.properties"); /* * JETTY Parameters */ - @Parameter(names = { "--nio" }, description = "Use NIO Connector else use Socket Connector.") + @Parameter(names = { "--useNio" }, description = "Use NIO Connector else use Socket Connector.") public Boolean useNIO = fileSettings.getBoolean(Keys.server.useNio, true); - @Parameter(names = "--port", description = "HTTP port for to serve. (port <= 0 will disable this connector)") + @Parameter(names = "--httpPort", description = "HTTP port for to serve. (port <= 0 will disable this connector)") public Integer port = fileSettings.getInteger(Keys.server.httpPort, 80); - @Parameter(names = "--securePort", description = "HTTPS port to serve. (port <= 0 will disable this connector)") + @Parameter(names = "--httpsPort", description = "HTTPS port to serve. (port <= 0 will disable this connector)") public Integer securePort = fileSettings.getInteger(Keys.server.httpsPort, 443); @Parameter(names = "--storePassword", description = "Password for SSL (https) keystore.") diff --git a/src/com/gitblit/GitBlitServlet.java b/src/com/gitblit/GitBlitServlet.java new file mode 100644 index 0000000..cb23e47 --- /dev/null +++ b/src/com/gitblit/GitBlitServlet.java @@ -0,0 +1,84 @@ +package com.gitblit; + +import java.io.IOException; +import java.text.MessageFormat; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jgit.http.server.GitServlet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.Constants.AccessRestrictionType; +import com.gitblit.wicket.models.RepositoryModel; + +public class GitBlitServlet extends GitServlet { + + private static final long serialVersionUID = 1L; + + private final Logger logger = LoggerFactory.getLogger(GitBlitServlet.class); + + public GitBlitServlet() { + super(); + } + + @Override + protected void service(final HttpServletRequest req, final HttpServletResponse rsp) throws ServletException, IOException { + // admins have full git access to all repositories + if (req.isUserInRole(Constants.ADMIN_ROLE)) { + // admins can do whatever + super.service(req, rsp); + return; + } + + // try to intercept repository names for authenticated access + String url = req.getRequestURI().substring(req.getServletPath().length()); + if (url.charAt(0) == '/' && url.length() > 1) { + url = url.substring(1); + } + int forwardSlash = url.indexOf('/'); + if (forwardSlash > -1) { + String repository = url.substring(0, forwardSlash); + String function = url.substring(forwardSlash + 1); + String query = req.getQueryString(); + RepositoryModel model = GitBlit.self().getRepositoryModel(repository); + if (model != null) { + if (model.accessRestriction.atLeast(AccessRestrictionType.PUSH)) { + boolean authorizedUser = req.isUserInRole(repository); + if (function.startsWith("git-receive-pack") || (query.indexOf("service=git-receive-pack") > -1)) { + // Push request + boolean pushRestricted = model.accessRestriction.atLeast(AccessRestrictionType.PUSH); + if (!pushRestricted || (pushRestricted && authorizedUser)) { + // push-unrestricted or push-authorized + super.service(req, rsp); + return; + } else { + // user is unauthorized to push to this repository + logger.warn(MessageFormat.format("user {0} is not authorized to push to {1} ", req.getUserPrincipal().getName(), repository)); + rsp.sendError(HttpServletResponse.SC_FORBIDDEN, MessageFormat.format("you are not authorized to push to {0} ", repository)); + return; + } + } else if (function.startsWith("git-upload-pack") || (query.indexOf("service=git-upload-pack") > -1)) { + // Clone request + boolean cloneRestricted = model.accessRestriction.atLeast(AccessRestrictionType.CLONE); + if (!cloneRestricted || (cloneRestricted && authorizedUser)) { + // clone-unrestricted or clone-authorized + super.service(req, rsp); + return; + } else { + // user is unauthorized to clone this repository + logger.warn(MessageFormat.format("user {0} is not authorized to clone {1} ", req.getUserPrincipal().getName(), repository)); + rsp.sendError(HttpServletResponse.SC_FORBIDDEN, MessageFormat.format("you are not authorized to clone {0} ", repository)); + return; + } + } + } + } + } + + // pass-through to git servlet + super.service(req, rsp); + } +} diff --git a/src/com/gitblit/ILoginService.java b/src/com/gitblit/ILoginService.java index b58f4f1..fc2801d 100644 --- a/src/com/gitblit/ILoginService.java +++ b/src/com/gitblit/ILoginService.java @@ -1,10 +1,17 @@ package com.gitblit; -import com.gitblit.wicket.User; +import com.gitblit.wicket.models.User; public interface ILoginService { User authenticate(String username, char[] password); User authenticate(char[] cookie); + + User getUserModel(String username); + + boolean updateUserModel(User model); + + boolean deleteUserModel(User model); + } diff --git a/src/com/gitblit/JettyLoginService.java b/src/com/gitblit/JettyLoginService.java index 5173d21..ddd3722 100644 --- a/src/com/gitblit/JettyLoginService.java +++ b/src/com/gitblit/JettyLoginService.java @@ -1,14 +1,33 @@ package com.gitblit; -import org.eclipse.jetty.security.HashLoginService; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Map; +import java.util.Properties; + +import javax.security.auth.Subject; + +import org.eclipse.jetty.http.security.Credential; +import org.eclipse.jetty.security.IdentityService; +import org.eclipse.jetty.security.MappedLoginService; import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.util.log.Log; -import com.gitblit.wicket.User; +import com.gitblit.utils.StringUtils; +import com.gitblit.wicket.models.User; -public class JettyLoginService extends HashLoginService implements ILoginService { +public class JettyLoginService extends MappedLoginService implements ILoginService { - public JettyLoginService(String realmFile) { - super(Constants.NAME, realmFile); + private final File realmFile; + + public JettyLoginService(File realmFile) { + super(); + setName(Constants.NAME); + this.realmFile = realmFile; } @Override @@ -17,10 +36,19 @@ if (identity == null || identity.equals(UserIdentity.UNAUTHENTICATED_IDENTITY)) { return null; } - User user = new User(username, password); + User user = new User(username); + user.setCookie(StringUtils.getSHA1((Constants.NAME + username + new String(password)))); user.canAdmin(identity.isUserInRole(Constants.ADMIN_ROLE, null)); - user.canClone(identity.isUserInRole(Constants.PULL_ROLE, null)); - user.canPush(identity.isUserInRole(Constants.PUSH_ROLE, null)); + + // Add repositories + for (Principal principal : identity.getSubject().getPrincipals()) { + if (principal instanceof RolePrincipal) { + RolePrincipal role = (RolePrincipal) principal; + if (role.getName().charAt(0) != '#') { + user.addRepository(role.getName().substring(1)); + } + } + } return user; } @@ -29,4 +57,140 @@ // TODO cookie login return null; } + + @Override + public User getUserModel(String username) { + User model = new User(username); + UserIdentity identity = _users.get(username); + Subject subject = identity.getSubject(); + for (Principal principal : subject.getPrincipals()) { + if (principal instanceof RolePrincipal) { + RolePrincipal role = (RolePrincipal) principal; + String name = role.getName(); + switch (name.charAt(0)) { + case '#': + // Permissions + if (name.equalsIgnoreCase(Constants.ADMIN_ROLE)) { + model.canAdmin(true); + } + break; + default: + model.addRepository(name.substring(1)); + } + } + } + return model; + } + + @Override + public boolean updateUserModel(User model) { + try { + Properties properties = new Properties(); + FileReader reader = new FileReader(realmFile); + properties.load(reader); + reader.close(); + + ArrayList<String> roles = new ArrayList<String>(); + + // Repositories + roles.addAll(model.getRepositories()); + + // Permissions + if (model.canAdmin()) { + roles.add(Constants.ADMIN_ROLE); + } + + StringBuilder sb = new StringBuilder(); + sb.append(model.getPassword()); + sb.append(','); + for (String role : roles) { + sb.append(role); + sb.append(','); + } + // trim trailing comma + sb.setLength(sb.length() - 1); + + // Update realm file + File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp"); + FileWriter writer = new FileWriter(realmFileCopy); + properties.put(model.getUsername(), sb.toString()); + properties.store(writer, null); + writer.close(); + realmFile.delete(); + realmFileCopy.renameTo(realmFile); + + // Update login service + putUser(model.getUsername(), Credential.getCredential(model.getPassword()), roles.toArray(new String[0])); + return true; + } catch (Throwable t) { + t.printStackTrace(); + } + return false; + } + + @Override + public boolean deleteUserModel(User model) { + try { + // Read realm file + Properties properties = new Properties(); + FileReader reader = new FileReader(realmFile); + properties.load(reader); + reader.close(); + properties.remove(model.getUsername()); + + // Update realm file + File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp"); + FileWriter writer = new FileWriter(realmFileCopy); + properties.store(writer, null); + writer.close(); + realmFile.delete(); + realmFileCopy.renameTo(realmFile); + + // Drop user from map + _users.remove(model.getUsername()); + return true; + } catch (Throwable t) { + t.printStackTrace(); + } + return false; + } + + /* ------------------------------------------------------------ */ + @Override + public void loadUsers() throws IOException { + if (realmFile == null) + return; + + if (Log.isDebugEnabled()) + Log.debug("Load " + this + " from " + realmFile); + Properties properties = new Properties(); + FileReader reader = new FileReader(realmFile); + properties.load(reader); + reader.close(); + + // Map Users + for (Map.Entry<Object, Object> entry : properties.entrySet()) { + String username = ((String) entry.getKey()).trim(); + String credentials = ((String) entry.getValue()).trim(); + String roles = null; + int c = credentials.indexOf(','); + if (c > 0) { + roles = credentials.substring(c + 1).trim(); + credentials = credentials.substring(0, c).trim(); + } + + if (username != null && username.length() > 0 && credentials != null && credentials.length() > 0) { + String[] roleArray = IdentityService.NO_ROLES; + if (roles != null && roles.length() > 0) { + roleArray = roles.split(","); + } + putUser(username, Credential.getCredential(credentials), roleArray); + } + } + } + + @Override + protected UserIdentity loadUser(String username) { + return null; + } } diff --git a/src/com/gitblit/wicket/AuthorizationStrategy.java b/src/com/gitblit/wicket/AuthorizationStrategy.java index 3e7df36..c4560ad 100644 --- a/src/com/gitblit/wicket/AuthorizationStrategy.java +++ b/src/com/gitblit/wicket/AuthorizationStrategy.java @@ -7,6 +7,7 @@ import com.gitblit.GitBlit; import com.gitblit.Keys; +import com.gitblit.wicket.models.User; import com.gitblit.wicket.pages.RepositoriesPage; public class AuthorizationStrategy extends AbstractPageAuthorizationStrategy implements IUnauthorizedComponentInstantiationListener { diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties index 39bdd29..f2fe232 100644 --- a/src/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/com/gitblit/wicket/GitBlitWebApp.properties @@ -70,11 +70,19 @@ gb.rename = rename gb.delete = delete gb.docs = docs -gb.restrictedAccess = restricted access +gb.accessRestriction = access restriction gb.name = name -gb.group = group gb.description = description gb.enableTickets = enable tickets gb.enableDocs = enable docs gb.save = save -gb.showRemoteBranches = show remote branches \ No newline at end of file +gb.showRemoteBranches = show remote branches +gb.editUsers = edit users +gb.password = password +gb.confirmPassword = confirm password +gb.repositories = repositories +gb.canAdmin can admin +gb.notRestricted = open repository +gb.cloneRestricted = clone-restricted repository +gb.pushRestricted = push-restricted repository +gb.viewRestricted = view-restricted repository \ No newline at end of file diff --git a/src/com/gitblit/wicket/GitBlitWebSession.java b/src/com/gitblit/wicket/GitBlitWebSession.java index 58ba495..c6cc36d 100644 --- a/src/com/gitblit/wicket/GitBlitWebSession.java +++ b/src/com/gitblit/wicket/GitBlitWebSession.java @@ -7,6 +7,8 @@ import org.apache.wicket.protocol.http.WebSession; import org.apache.wicket.protocol.http.request.WebClientInfo; +import com.gitblit.wicket.models.User; + public final class GitBlitWebSession extends WebSession { private static final long serialVersionUID = 1L; diff --git a/src/com/gitblit/wicket/LoginPage.java b/src/com/gitblit/wicket/LoginPage.java index 3f8206e..9e26a62 100644 --- a/src/com/gitblit/wicket/LoginPage.java +++ b/src/com/gitblit/wicket/LoginPage.java @@ -18,6 +18,7 @@ import com.gitblit.Constants; import com.gitblit.GitBlit; import com.gitblit.Keys; +import com.gitblit.wicket.models.User; public class LoginPage extends WebPage { diff --git a/src/com/gitblit/wicket/RepositoryPage.java b/src/com/gitblit/wicket/RepositoryPage.java index 1f88075..78fd33c 100644 --- a/src/com/gitblit/wicket/RepositoryPage.java +++ b/src/com/gitblit/wicket/RepositoryPage.java @@ -161,11 +161,17 @@ protected RepositoryModel getRepositoryModel() { if (m == null) { - m = GitBlit.self().getRepositoryModel(repositoryName); + RepositoryModel model = GitBlit.self().getRepositoryModel(GitBlitWebSession.get().getUser(), repositoryName); + if (model == null) { + error("Unauthorized access for repository " + repositoryName); + redirectToInterceptPage(new RepositoriesPage()); + return null; + } + m = model; } return m; } - + protected RevCommit getCommit() { RevCommit commit = JGitUtils.getCommit(r, objectId); if (commit == null) { diff --git a/src/com/gitblit/wicket/User.java b/src/com/gitblit/wicket/User.java deleted file mode 100644 index bd5e8c9..0000000 --- a/src/com/gitblit/wicket/User.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.gitblit.wicket; - -import java.io.Serializable; - -import com.gitblit.Constants; -import com.gitblit.utils.StringUtils; - -public class User implements Serializable { - - private static final long serialVersionUID = 1L; - - private String username; - private String cookie; - private boolean canAdmin = false; - private boolean canClone = false; - private boolean canPush = false; - - public User(String username, char[] password) { - this.username = username; - this.cookie = StringUtils.getSHA1((Constants.NAME + username + new String(password))); - } - - public void canAdmin(boolean value) { - canAdmin = value; - } - - public boolean canAdmin() { - return canAdmin; - } - - public void canClone(boolean value) { - canClone = value; - } - - public boolean canClone() { - return canClone; - } - - public void canPush(boolean value) { - canPush = value; - } - - public boolean canPush() { - return canPush; - } - - public String getCookie() { - return cookie; - } - - public String toString() { - return username; - } -} diff --git a/src/com/gitblit/wicket/WicketUtils.java b/src/com/gitblit/wicket/WicketUtils.java index bf2bcb9..f0ccbf4 100644 --- a/src/com/gitblit/wicket/WicketUtils.java +++ b/src/com/gitblit/wicket/WicketUtils.java @@ -227,6 +227,10 @@ public static int getPage(PageParameters params) { return params.getInt("page", 1); // index from 1 } + + public static String getUsername(PageParameters params) { + return params.getString("user", ""); + } public static Label createDateLabel(String wicketId, Date date, TimeZone timeZone) { DateFormat df = new SimpleDateFormat(GitBlit.self().settings().getString(Keys.web.datestampShortFormat, "MM/dd/yy")); diff --git a/src/com/gitblit/wicket/models/RepositoryModel.java b/src/com/gitblit/wicket/models/RepositoryModel.java index d21cff6..43a7ac1 100644 --- a/src/com/gitblit/wicket/models/RepositoryModel.java +++ b/src/com/gitblit/wicket/models/RepositoryModel.java @@ -3,28 +3,29 @@ import java.io.Serializable; import java.util.Date; +import com.gitblit.Constants.AccessRestrictionType; + public class RepositoryModel implements Serializable { private static final long serialVersionUID = 1L; public String name; public String description; public String owner; - public String group; public Date lastChange; public boolean hasCommits; public boolean showRemoteBranches; public boolean useTickets; public boolean useDocs; - public boolean useRestrictedAccess; + public AccessRestrictionType accessRestriction; public RepositoryModel() { - + } - + public RepositoryModel(String name, String description, String owner, Date lastchange) { this.name = name; this.description = description; this.owner = owner; this.lastChange = lastchange; - } + } } \ No newline at end of file diff --git a/src/com/gitblit/wicket/models/User.java b/src/com/gitblit/wicket/models/User.java new file mode 100644 index 0000000..0784839 --- /dev/null +++ b/src/com/gitblit/wicket/models/User.java @@ -0,0 +1,89 @@ +package com.gitblit.wicket.models; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import com.gitblit.Constants.AccessRestrictionType; + +public class User implements Serializable { + + private static final long serialVersionUID = 1L; + + private String username; + private String password; + private String cookie; + private boolean canAdmin = false; + private List<String> repositories = new ArrayList<String>(); + + public User(String username) { + this.username = username; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public void canAdmin(boolean value) { + canAdmin = value; + } + + public boolean canAdmin() { + return canAdmin; + } + + public boolean canClone(RepositoryModel repository) { + return canAccess(repository, AccessRestrictionType.CLONE); + } + + public boolean canPush(RepositoryModel repository) { + return canAccess(repository, AccessRestrictionType.PUSH); + } + + public boolean canView(RepositoryModel repository) { + return canAccess(repository, AccessRestrictionType.VIEW); + } + + private boolean canAccess(RepositoryModel repository, AccessRestrictionType minimum) { + if (repository.accessRestriction.atLeast(minimum)) { + // repository is restricted, must check roles + return canAdmin || repositories.contains(repository.name); + } else { + // repository is not restricted + return true; + } + } + + public void setCookie(String cookie) { + this.cookie = cookie; + } + + public String getCookie() { + return cookie; + } + + public void setRepositories(List<String> repositories) { + this.repositories.clear(); + this.repositories.addAll(repositories); + } + + public void addRepository(String name) { + repositories.add(name.toLowerCase()); + } + + public List<String> getRepositories() { + return repositories; + } + + public String toString() { + return username; + } +} diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.html b/src/com/gitblit/wicket/pages/EditRepositoryPage.html index bc965e7..5872347 100644 --- a/src/com/gitblit/wicket/pages/EditRepositoryPage.html +++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.html @@ -4,8 +4,8 @@ xml:lang="en" lang="en"> -<body> <wicket:extend> +<body onload="document.getElementById('name').focus();"> <!-- Push content down to preserve header image --> <div style="padding-top:20px"></div> @@ -15,18 +15,18 @@ <form wicket:id="editForm"> <table class="plain"> <tbody> - <tr><th><wicket:message key="gb.name"></wicket:message></th><td class="edit"><input type="text" wicket:id="name" size="30" tabindex="1" /></td></tr> - <tr><th><wicket:message key="gb.description"></wicket:message></th><td class="edit"><input type="text" wicket:id="description" size="80" tabindex="2" /></td></tr> - <tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><input type="text" wicket:id="owner" size="30" tabindex="3" /></td></tr> - <tr><th><wicket:message key="gb.group"></wicket:message></th><td class="edit"><input type="text" wicket:id="group" size="30" tabindex="4" /></td></tr> + <tr><th><wicket:message key="gb.name"></wicket:message></th><td class="edit"><input type="text" wicket:id="name" id="name" size="40" tabindex="1" /></td></tr> + <tr><th><wicket:message key="gb.description"></wicket:message></th><td class="edit"><input type="text" wicket:id="description" size="40" tabindex="2" /></td></tr> + <tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><input type="text" wicket:id="owner" size="40" tabindex="3" /></td></tr> + <tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select wicket:id="accessRestriction" tabindex="4" /></td></tr> <tr><th><wicket:message key="gb.enableTickets"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useTickets" tabindex="5" /> <i>distributed Ticgit issues</i></td></tr> - <tr><th><wicket:message key="gb.enableDocs"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useDocs" tabindex="6" /> <i>enumerates repository Markdown documentation</i></td></tr> + <tr><th><wicket:message key="gb.enableDocs"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useDocs" tabindex="6" /> <i>enumerates Markdown documentation in repository</i></td></tr> <tr><th><wicket:message key="gb.showRemoteBranches"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="showRemoteBranches" tabindex="7" /> <i>show remote branches</i></td></tr> - <tr><td class="edit" colspan="2"><input type="submit" value="Save" wicket:message="value:gb.save" tabindex="8" /></td></tr> + <tr><th></th><td class="editButton"><input type="submit" value="Save" wicket:message="value:gb.save" tabindex="8" /></td></tr> </tbody> </table> </form> -</wicket:extend> </body> +</wicket:extend> </html> \ No newline at end of file diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.java b/src/com/gitblit/wicket/pages/EditRepositoryPage.java index 6bf1659..2d2b0ae 100644 --- a/src/com/gitblit/wicket/pages/EditRepositoryPage.java +++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.java @@ -1,13 +1,16 @@ package com.gitblit.wicket.pages; +import java.util.Arrays; import java.util.Date; import org.apache.wicket.PageParameters; import org.apache.wicket.markup.html.form.CheckBox; +import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.TextField; import org.apache.wicket.model.CompoundPropertyModel; +import com.gitblit.Constants.AccessRestrictionType; import com.gitblit.GitBlit; import com.gitblit.GitBlitException; import com.gitblit.wicket.AdminPage; @@ -40,7 +43,7 @@ if (isCreate) { super.setupPage("", getString("gb.newRepository")); } else { - super.setupPage("", getString("gb.edit")); + super.setupPage("", getString("gb.edit") + " " + repositoryModel.name); } CompoundPropertyModel<RepositoryModel> model = new CompoundPropertyModel<RepositoryModel>(repositoryModel); Form<RepositoryModel> form = new Form<RepositoryModel>("editForm", model) { @@ -59,12 +62,12 @@ setResponsePage(RepositoriesPage.class); } }; - + // field names reflective match RepositoryModel fields form.add(new TextField<String>("name").setEnabled(isCreate)); form.add(new TextField<String>("description")); form.add(new TextField<String>("owner")); - form.add(new TextField<String>("group")); + form.add(new DropDownChoice<AccessRestrictionType>("accessRestriction", Arrays.asList(AccessRestrictionType.values()))); form.add(new CheckBox("useTickets")); form.add(new CheckBox("useDocs")); form.add(new CheckBox("showRemoteBranches")); diff --git a/src/com/gitblit/wicket/pages/EditUserPage.html b/src/com/gitblit/wicket/pages/EditUserPage.html new file mode 100644 index 0000000..57407d2 --- /dev/null +++ b/src/com/gitblit/wicket/pages/EditUserPage.html @@ -0,0 +1,29 @@ +<!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('username').focus();"> + <!-- Push content down to preserve header image --> + <div style="padding-top:20px"></div> + + <div style="text-align:center;" wicket:id="feedback">[Feedback Panel]</div> + + <!-- Repository Table --> + <form wicket:id="editForm"> + <table class="plain"> + <tbody> + <tr><th><wicket:message key="gb.name"></wicket:message></th><td class="edit"><input type="text" wicket:id="username" id="username" size="30" tabindex="1" /></td></tr> + <tr><th><wicket:message key="gb.password"></wicket:message></th><td class="edit"><input type="password" wicket:id="password" size="30" tabindex="2" /></td></tr> + <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>can administer Git:Blit server</i></td></tr> + <tr><th style="vertical-align: top;"><wicket:message key="gb.repositories"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr> + <tr><th></th><td class="editButton"><input type="submit" value="Save" wicket:message="value:gb.save" tabindex="7" /></td></tr> + </tbody> + </table> + </form> +</body> +</wicket:extend> +</html> \ No newline at end of file diff --git a/src/com/gitblit/wicket/pages/EditUserPage.java b/src/com/gitblit/wicket/pages/EditUserPage.java new file mode 100644 index 0000000..84bf7bc --- /dev/null +++ b/src/com/gitblit/wicket/pages/EditUserPage.java @@ -0,0 +1,97 @@ +package com.gitblit.wicket.pages; + +import java.util.ArrayList; +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.CheckBox; +import org.apache.wicket.markup.html.form.ChoiceRenderer; +import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.markup.html.form.PasswordTextField; +import org.apache.wicket.markup.html.form.TextField; +import org.apache.wicket.model.CompoundPropertyModel; +import org.apache.wicket.model.Model; +import org.apache.wicket.model.util.CollectionModel; +import org.apache.wicket.model.util.ListModel; +import org.eclipse.jetty.http.security.Credential.MD5; + +import com.gitblit.GitBlit; +import com.gitblit.GitBlitException; +import com.gitblit.wicket.AdminPage; +import com.gitblit.wicket.BasePage; +import com.gitblit.wicket.WicketUtils; +import com.gitblit.wicket.models.User; + +@AdminPage +public class EditUserPage extends BasePage { + + private final boolean isCreate; + + public EditUserPage() { + // create constructor + super(); + isCreate = true; + setupPage(new User("")); + } + + public EditUserPage(PageParameters params) { + // edit constructor + super(params); + isCreate = false; + String name = WicketUtils.getUsername(params); + User model = GitBlit.self().getUser(name); + setupPage(model); + } + + protected void setupPage(final User userModel) { + if (isCreate) { + super.setupPage("", getString("gb.newUser")); + } else { + super.setupPage("", getString("gb.edit")); + } + final Model<String> confirmPassword = new Model<String>(); + CompoundPropertyModel<User> model = new CompoundPropertyModel<User>(userModel); + + List<String> repos = GitBlit.self().getRepositoryList(); + repos.add(0, "*"); // all repositories wildcard + final Palette<String> repositories = new Palette<String>("repositories", new ListModel<String>(userModel.getRepositories()), new CollectionModel<String>(repos), new ChoiceRenderer<String>("", ""), 10, false); + Form<User> form = new Form<User>("editForm", model) { + + private static final long serialVersionUID = 1L; + + @Override + protected void onSubmit() { + if (!userModel.getPassword().equals(confirmPassword.getObject())) { + error("Passwords do not match!"); + return; + } + userModel.setPassword(MD5.digest(userModel.getPassword())); + + Iterator<String> selectedRepositories = repositories.getSelectedChoices(); + List<String> repos = new ArrayList<String>(); + while (selectedRepositories.hasNext()) { + repos.add(selectedRepositories.next()); + } + userModel.setRepositories(repos); + try { + GitBlit.self().editUserModel(userModel, isCreate); + } catch (GitBlitException e) { + error(e.getMessage()); + return; + } + setRedirect(true); + setResponsePage(EditUserPage.class); + } + }; + + // field names reflective match UserModel fields + form.add(new TextField<String>("username").setEnabled(isCreate)); + form.add(new PasswordTextField("password")); + form.add(new PasswordTextField("confirmPassword", confirmPassword)); + form.add(new CheckBox("canAdmin")); + form.add(repositories); + add(form); + } +} diff --git a/src/com/gitblit/wicket/pages/RepositoriesPage.html b/src/com/gitblit/wicket/pages/RepositoriesPage.html index 09de115..3016f64 100644 --- a/src/com/gitblit/wicket/pages/RepositoriesPage.html +++ b/src/com/gitblit/wicket/pages/RepositoriesPage.html @@ -41,7 +41,7 @@ <wicket:fragment wicket:id="adminLinks"> <!-- page nav links --> <div class="page_nav"> - <a wicket:id="newRepository"><wicket:message key="gb.newRepository"></wicket:message></a> | <a wicket:id="newUser"><wicket:message key="gb.newUser"></wicket:message></a> + <a wicket:id="newRepository"><wicket:message key="gb.newRepository"></wicket:message></a> | <a wicket:id="newUser"><wicket:message key="gb.newUser"></wicket:message></a> | <a wicket:id="editUsers"><wicket:message key="gb.editUsers"></wicket:message></a> </div> </wicket:fragment> diff --git a/src/com/gitblit/wicket/pages/RepositoriesPage.java b/src/com/gitblit/wicket/pages/RepositoriesPage.java index 111084c..9f3aa2a 100644 --- a/src/com/gitblit/wicket/pages/RepositoriesPage.java +++ b/src/com/gitblit/wicket/pages/RepositoriesPage.java @@ -33,6 +33,7 @@ import com.gitblit.wicket.LinkPanel; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.models.RepositoryModel; +import com.gitblit.wicket.models.User; public class RepositoriesPage extends BasePage { @@ -50,7 +51,8 @@ Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this); adminLinks.add(new BookmarkablePageLink<Void>("newRepository", EditRepositoryPage.class)); - adminLinks.add(new BookmarkablePageLink<Void>("newUser", RepositoriesPage.class)); + adminLinks.add(new BookmarkablePageLink<Void>("newUser", EditUserPage.class)); + adminLinks.add(new BookmarkablePageLink<Void>("editUsers", RepositoriesPage.class)); add(adminLinks.setVisible(showAdmin)); // display an error message cached from a redirect @@ -59,7 +61,7 @@ error(cachedMessage); System.out.println("displayed message"); } - + // Load the markdown welcome message String messageSource = GitBlit.self().settings().getString(Keys.web.repositoriesMessage, "gitblit"); String message = ""; @@ -97,7 +99,8 @@ } add(repositoriesMessage); - List<RepositoryModel> rows = GitBlit.self().getRepositoryModels(); + User user = GitBlitWebSession.get().getUser(); + List<RepositoryModel> rows = GitBlit.self().getRepositoryModels(user); DataProvider dp = new DataProvider(rows); DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repository", dp) { private static final long serialVersionUID = 1L; @@ -113,27 +116,38 @@ } else { // New repository item.add(new Label("repositoryName", entry.name + "<span class='empty'>(empty)</span>").setEscapeModelStrings(false)); - item.add(new Label("repositoryDescription", entry.description)); + item.add(new Label("repositoryDescription", entry.description)); } - + if (entry.useTickets) { item.add(WicketUtils.newImage("ticketsIcon", "bug_16x16.png", getString("gb.tickets"))); } else { - item.add(WicketUtils.newClearPixel("ticketsIcon")); + item.add(WicketUtils.newBlankImage("ticketsIcon")); } - + if (entry.useDocs) { item.add(WicketUtils.newImage("docsIcon", "book_16x16.png", getString("gb.docs"))); } else { - item.add(WicketUtils.newClearPixel("docsIcon")); + item.add(WicketUtils.newBlankImage("docsIcon")); } - - if (entry.useRestrictedAccess) { - item.add(WicketUtils.newImage("restrictedAccessIcon", "lock_16x16.png", getString("gb.restrictedAccess"))); - } else { - item.add(WicketUtils.newClearPixel("restrictedAccessIcon")); + + switch (entry.accessRestriction) { + case NONE: + item.add(WicketUtils.newBlankImage("restrictedAccessIcon")); + break; + case PUSH: + item.add(WicketUtils.newImage("restrictedAccessIcon", "lock_go_16x16.png", getString("gb.pushRestricted"))); + break; + case CLONE: + item.add(WicketUtils.newImage("restrictedAccessIcon", "lock_pull_16x16.png", getString("gb.cloneRestricted"))); + break; + case VIEW: + item.add(WicketUtils.newImage("restrictedAccessIcon", "shield_16x16.png", getString("gb.viewRestricted"))); + break; + default: + item.add(WicketUtils.newBlankImage("restrictedAccessIcon")); } - + item.add(new Label("repositoryOwner", entry.owner)); String lastChange = TimeUtils.timeAgo(entry.lastChange); diff --git a/src/com/gitblit/wicket/resources/gitblit.css b/src/com/gitblit/wicket/resources/gitblit.css index 3d0a1cd..13e761d 100644 --- a/src/com/gitblit/wicket/resources/gitblit.css +++ b/src/com/gitblit/wicket/resources/gitblit.css @@ -444,8 +444,13 @@ padding: 8px; } -table.plain td.edit { +table.plain td.edit { padding: 3px; +} + +table.plain td.editButton { + padding:0px; + padding-top: 10px; } table.plain td.edit input { @@ -517,6 +522,19 @@ color: black; font-weight: bold; } + +table.palette { border:0;} +table.palette td.header { + font-weight: bold; + background-color: #D2C3AF !important; + padding: 3px !important; + border: 1px solid #808080 !important; + border-bottom: 0px solid !important; + border-radius: 3px 3px 0 0; +} +table.palette td.pane { + padding: 0px; +} tr th a { padding-right: 15px; background-position: right; background-repeat:no-repeat; } tr th.wicket_orderDown a {background-image: url(arrow_down.png); } @@ -619,22 +637,22 @@ span .otherRef { background-color: #ffaaff; - border-color: #ffccff #ff00ee #ff00ee #ffccff; + border-color: #ff00ee; } span .remoteRef { background-color: #cAc2f5; - border-color: #ccccff #0033cc #0033cc #ccccff; + border-color: #6c6cbf; } span .tagRef { background-color: #ffffaa; - border-color: #ffcc00 #ffcc00 #ffcc00 #ffcc00; + border-color: #ffcc00; } span .headRef { background-color: #ccffcc; - border-color: #ccffcc #00cc33 #00cc33 #ccffcc; + border-color: #00cc33; } .feedbackPanelERROR { diff --git a/src/com/gitblit/wicket/resources/lock_go_16x16.png b/src/com/gitblit/wicket/resources/lock_go_16x16.png new file mode 100644 index 0000000..63d4285 --- /dev/null +++ b/src/com/gitblit/wicket/resources/lock_go_16x16.png Binary files differ diff --git a/src/com/gitblit/wicket/resources/lock_pull_16x16.png b/src/com/gitblit/wicket/resources/lock_pull_16x16.png new file mode 100644 index 0000000..85c5c53 --- /dev/null +++ b/src/com/gitblit/wicket/resources/lock_pull_16x16.png Binary files differ diff --git a/src/com/gitblit/wicket/resources/shield_16x16.png b/src/com/gitblit/wicket/resources/shield_16x16.png new file mode 100644 index 0000000..4eb8031 --- /dev/null +++ b/src/com/gitblit/wicket/resources/shield_16x16.png Binary files differ diff --git a/users.properties b/users.properties index eddad82..0b22d82 100644 --- a/users.properties +++ b/users.properties @@ -1,2 +1,3 @@ -test: test,pull -admin: admin,pull,push,admin +#Wed May 11 21:30:28 EDT 2011 +admin=admin,\#admin +test=test -- Gitblit v1.9.1