@@ -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!! | |||
@@ -24,9 +24,6 @@ git.cloneUrl = https://localhost/git/ | |||
# 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 @@ server.log4jPattern.linux = | |||
# 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 @@ server.httpBindInterface = localhost | |||
# 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. |
@@ -6,14 +6,41 @@ public class Constants { | |||
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; | |||
} |
@@ -19,9 +19,10 @@ import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; | |||
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 @@ public class GitBlit implements ServletContextListener { | |||
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 @@ public class GitBlit implements ServletContextListener { | |||
} | |||
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 @@ public class GitBlit implements ServletContextListener { | |||
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 @@ public class GitBlit implements ServletContextListener { | |||
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 @@ public class GitBlit implements ServletContextListener { | |||
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(); |
@@ -56,7 +56,6 @@ import org.eclipse.jetty.util.log.Log; | |||
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 @@ public class GitBlitServer { | |||
// 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 @@ public class GitBlitServer { | |||
handler = rootContext; | |||
} | |||
} else { | |||
logger.info("Git servlet pull/push disabled"); | |||
logger.info("Git servlet clone/push disabled"); | |||
handler = rootContext; | |||
} | |||
@@ -448,37 +445,31 @@ public class GitBlitServer { | |||
@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.") |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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.utils.StringUtils; | |||
import com.gitblit.wicket.models.User; | |||
import com.gitblit.wicket.User; | |||
public class JettyLoginService extends MappedLoginService implements ILoginService { | |||
public class JettyLoginService extends HashLoginService implements ILoginService { | |||
private final File realmFile; | |||
public JettyLoginService(String realmFile) { | |||
super(Constants.NAME, realmFile); | |||
public JettyLoginService(File realmFile) { | |||
super(); | |||
setName(Constants.NAME); | |||
this.realmFile = realmFile; | |||
} | |||
@Override | |||
@@ -17,10 +36,19 @@ public class JettyLoginService extends HashLoginService implements ILoginService | |||
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 @@ public class JettyLoginService extends HashLoginService implements ILoginService | |||
// 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; | |||
} | |||
} |
@@ -7,6 +7,7 @@ import org.apache.wicket.authorization.strategies.page.AbstractPageAuthorization | |||
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 { |
@@ -70,11 +70,19 @@ gb.searchTooltip = Search Git:Blit | |||
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 | |||
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 |
@@ -7,6 +7,8 @@ import org.apache.wicket.Session; | |||
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; |
@@ -18,6 +18,7 @@ import org.apache.wicket.protocol.http.WebResponse; | |||
import com.gitblit.Constants; | |||
import com.gitblit.GitBlit; | |||
import com.gitblit.Keys; | |||
import com.gitblit.wicket.models.User; | |||
public class LoginPage extends WebPage { | |||
@@ -161,11 +161,17 @@ public abstract class RepositoryPage extends BasePage { | |||
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) { |
@@ -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; | |||
} | |||
} |
@@ -227,6 +227,10 @@ public class WicketUtils { | |||
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")); |
@@ -3,28 +3,29 @@ package com.gitblit.wicket.models; | |||
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; | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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> |
@@ -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 @@ public class EditRepositoryPage extends BasePage { | |||
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 @@ public class EditRepositoryPage extends BasePage { | |||
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")); |
@@ -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> |
@@ -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); | |||
} | |||
} |
@@ -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> | |||
@@ -33,6 +33,7 @@ import com.gitblit.wicket.GitBlitWebSession; | |||
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 @@ public class RepositoriesPage extends BasePage { | |||
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 @@ public class RepositoriesPage extends BasePage { | |||
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 @@ public class RepositoriesPage extends BasePage { | |||
} | |||
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 @@ public class RepositoriesPage extends BasePage { | |||
} 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); |
@@ -444,10 +444,15 @@ table.plain { | |||
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 { | |||
margin: 0px; | |||
outline: 1px solid transparent; | |||
@@ -518,6 +523,19 @@ table.repositories th.wicket_orderDown a, table.repositories th.wicket_orderUp a | |||
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); } | |||
tr th.wicket_orderUp a { background-image: url(arrow_up.png); } | |||
@@ -619,22 +637,22 @@ span .tagRef a:hover, span .headRef a:hover, span .remoteRef a:hover, span .othe | |||
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 { |
@@ -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 |