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" /> &nbsp;<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" /> &nbsp;<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" /> &nbsp;<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" /> &nbsp;<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" /> &nbsp;<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