From 6cca8699f98a606ff19e88d40a8a2535fdc340e7 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Fri, 06 Apr 2012 18:01:58 -0400
Subject: [PATCH] Skeleton LdapUserService based on John Cryiger's implementation

---
 src/com/gitblit/wicket/panels/UsersPanel.java        |    3 
 src/com/gitblit/LdapUserService.java                 |  200 ++++++++++++++++++++++
 src/com/gitblit/ConfigUserService.java               |   25 ++
 src/com/gitblit/IUserService.java                    |   16 +
 src/com/gitblit/wicket/pages/EditUserPage.java       |   18 +
 distrib/gitblit.properties                           |   46 +++++
 src/com/gitblit/IStoredSettings.java                 |   18 ++
 src/com/gitblit/FileUserService.java                 |   27 ++
 src/com/gitblit/GitBlit.java                         |   16 +
 src/com/gitblit/wicket/GitBlitWebApp.properties      |    4 
 src/com/gitblit/wicket/pages/EditTeamPage.java       |    5 
 src/com/gitblit/utils/ConnectionUtils.java           |   89 +++++++++
 src/com/gitblit/wicket/pages/BasePage.java           |    6 
 src/com/gitblit/wicket/pages/ChangePasswordPage.java |    6 
 src/com/gitblit/GitblitUserService.java              |   38 ++++
 15 files changed, 504 insertions(+), 13 deletions(-)

diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties
index 2846496..acceb88 100644
--- a/distrib/gitblit.properties
+++ b/distrib/gitblit.properties
@@ -136,6 +136,52 @@
 # SINCE 0.5.0 
 realm.minPasswordLength = 5
 
+# URL of the LDAP server.
+#
+# SINCE 1.0.0
+realm.ldap.server = ldap://my.ldap.server
+
+# The LDAP domain to prepend to all usernames during authentication.  If
+# unspecified, all logins must prepend the domain to their username.
+# e.g. mydomain
+#
+# SINCE 1.0.0
+realm.ldap.domain = 
+
+# Login username for LDAP searches.
+# The domain prefix may be omitted if it matches the domain specified in
+# *realm.ldap.domain*. If this value is unspecified, anonymous LDAP login will
+# be used.
+# 
+# e.g. mydomain\\username
+#
+# SINCE 1.0.0
+realm.ldap.username =
+
+# Login password for LDAP searches.
+#
+# SINCE 1.0.0
+realm.ldap.password =
+
+# The LdapUserService must be backed by another user service for standard user
+# and team management.
+# default: users.conf
+#
+# SINCE 1.0.0
+# RESTART REQUIRED
+realm.ldap.backingUserService = users.conf
+
+# Delegate team membership control to LDAP.
+#
+# If true, team user memberships will be specified by LDAP groups.  This will
+# disable team selection in Edit User and user selection in Edit Team.
+#
+# If false, LDAP will only be used for authentication and Gitblit will maintain
+# team memberships with the *realm.ldap.backingUserService*.
+#
+# SINCE 1.0.0
+realm.ldap.maintainTeams = false
+
 #
 # Gitblit Web Settings
 #
diff --git a/src/com/gitblit/ConfigUserService.java b/src/com/gitblit/ConfigUserService.java
index 8f47f7a..828ba76 100644
--- a/src/com/gitblit/ConfigUserService.java
+++ b/src/com/gitblit/ConfigUserService.java
@@ -100,6 +100,27 @@
 	}
 
 	/**
+	 * Does the user service support changes to credentials?
+	 * 
+	 * @return true or false
+	 * @since 1.0.0
+	 */
+	@Override
+	public boolean supportsCredentialChanges() {
+		return true;
+	}
+	
+	/**
+	 * Does the user service support changes to team memberships?
+	 * 
+	 * @return true or false
+	 * @since 1.0.0
+	 */	
+	public boolean supportsTeamMembershipChanges() {
+		return true;
+	}
+	
+	/**
 	 * Does the user service support cookie authentication?
 	 * 
 	 * @return true or false
@@ -656,7 +677,9 @@
 
 		// write users
 		for (UserModel model : users.values()) {
-			config.setString(USER, model.username, PASSWORD, model.password);
+			if (!StringUtils.isEmpty(model.password)) {
+				config.setString(USER, model.username, PASSWORD, model.password);
+			}
 
 			// user roles
 			List<String> roles = new ArrayList<String>();
diff --git a/src/com/gitblit/FileUserService.java b/src/com/gitblit/FileUserService.java
index 7842c31..b8d4a40 100644
--- a/src/com/gitblit/FileUserService.java
+++ b/src/com/gitblit/FileUserService.java
@@ -74,6 +74,27 @@
 	}
 
 	/**
+	 * Does the user service support changes to credentials?
+	 * 
+	 * @return true or false
+	 * @since 1.0.0
+	 */
+	@Override
+	public boolean supportsCredentialChanges() {
+		return true;
+	}
+
+	/**
+	 * Does the user service support changes to team memberships?
+	 * 
+	 * @return true or false
+	 * @since 1.0.0
+	 */	
+	public boolean supportsTeamMembershipChanges() {
+		return true;
+	}
+
+	/**
 	 * Does the user service support cookie authentication?
 	 * 
 	 * @return true or false
@@ -233,7 +254,9 @@
 			}
 
 			StringBuilder sb = new StringBuilder();
-			sb.append(model.password);
+			if (!StringUtils.isEmpty(model.password)) {
+				sb.append(model.password);
+			}
 			sb.append(',');
 			for (String role : roles) {
 				sb.append(role);
@@ -658,6 +681,8 @@
 					team.addRepositories(repositories);
 					team.addUsers(users);
 					team.addMailingLists(mailingLists);
+					team.preReceiveScripts.addAll(preReceive);
+					team.postReceiveScripts.addAll(postReceive);
 					teams.put(team.name.toLowerCase(), team);
 				} else {
 					// user definition
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index 7b557d7..6ed54dc 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -377,6 +377,22 @@
 		this.userService = userService;
 		this.userService.setup(settings);
 	}
+	
+	/**
+	 * 
+	 * @return true if the user service supports credential changes
+	 */
+	public boolean supportsCredentialChanges() {
+		return userService.supportsCredentialChanges();
+	}
+
+	/**
+	 * 
+	 * @return true if the user service supports team membership changes
+	 */
+	public boolean supportsTeamMembershipChanges() {
+		return userService.supportsTeamMembershipChanges();
+	}
 
 	/**
 	 * Authenticate a user based on a username and password.
diff --git a/src/com/gitblit/GitblitUserService.java b/src/com/gitblit/GitblitUserService.java
index 7462af0..1514b6b 100644
--- a/src/com/gitblit/GitblitUserService.java
+++ b/src/com/gitblit/GitblitUserService.java
@@ -25,6 +25,7 @@
 
 import com.gitblit.models.TeamModel;
 import com.gitblit.models.UserModel;
+import com.gitblit.utils.DeepCopier;
 
 /**
  * This class wraps the default user service and is recommended as the starting
@@ -112,6 +113,16 @@
 	}
 
 	@Override
+	public boolean supportsCredentialChanges() {
+		return serviceImpl.supportsCredentialChanges();
+	}
+
+	@Override
+	public boolean supportsTeamMembershipChanges() {
+		return serviceImpl.supportsTeamMembershipChanges();
+	}
+
+	@Override
 	public boolean supportsCookies() {
 		return serviceImpl.supportsCookies();
 	}
@@ -143,9 +154,27 @@
 
 	@Override
 	public boolean updateUserModel(String username, UserModel model) {
-		return serviceImpl.updateUserModel(username, model);
+		if (supportsCredentialChanges()) {
+			if (!supportsTeamMembershipChanges()) {
+				//  teams are externally controlled
+				model = DeepCopier.copy(model);
+				model.teams.clear();
+			}
+			return serviceImpl.updateUserModel(username, model);
+		}
+		if (model.username.equals(username)) {
+			// passwords are not persisted by the backing user service
+			model.password = null;
+			if (!supportsTeamMembershipChanges()) {
+				//  teams are externally controlled
+				model = DeepCopier.copy(model);
+				model.teams.clear();
+			}
+			return serviceImpl.updateUserModel(username, model);
+		}
+		logger.error("Users can not be renamed!");
+		return false;
 	}
-
 	@Override
 	public boolean deleteUserModel(UserModel model) {
 		return serviceImpl.deleteUserModel(model);
@@ -198,6 +227,11 @@
 
 	@Override
 	public boolean updateTeamModel(String teamname, TeamModel model) {
+		if (!supportsTeamMembershipChanges()) {
+			// teams are externally controlled
+			model = DeepCopier.copy(model);
+			model.users.clear();
+		}
 		return serviceImpl.updateTeamModel(teamname, model);
 	}
 
diff --git a/src/com/gitblit/IStoredSettings.java b/src/com/gitblit/IStoredSettings.java
index 2d8b605..2f45f09 100644
--- a/src/com/gitblit/IStoredSettings.java
+++ b/src/com/gitblit/IStoredSettings.java
@@ -157,6 +157,24 @@
 		}
 		return defaultValue;
 	}
+	
+	/**
+	 * Returns the string value for the specified key.  If the key does not
+	 * exist an exception is thrown.
+	 * 
+	 * @param key
+	 * @return key value
+	 */
+	public String getRequiredString(String name) {
+		Properties props = getSettings();
+		if (props.containsKey(name)) {
+			String value = props.getProperty(name);
+			if (value != null) {
+				return value.trim();
+			}
+		}		
+		throw new RuntimeException("Property (" + name + ") does not exist");
+	}
 
 	/**
 	 * Returns a list of space-separated strings from the specified key.
diff --git a/src/com/gitblit/IUserService.java b/src/com/gitblit/IUserService.java
index a5e04e3..334bbed 100644
--- a/src/com/gitblit/IUserService.java
+++ b/src/com/gitblit/IUserService.java
@@ -40,6 +40,22 @@
 	void setup(IStoredSettings settings);
 
 	/**
+	 * Does the user service support changes to credentials?
+	 * 
+	 * @return true or false
+	 * @since 1.0.0
+	 */	
+	boolean supportsCredentialChanges();
+	
+	/**
+	 * Does the user service support changes to team memberships?
+	 * 
+	 * @return true or false
+	 * @since 1.0.0
+	 */	
+	boolean supportsTeamMembershipChanges();
+	
+	/**
 	 * Does the user service support cookie authentication?
 	 * 
 	 * @return true or false
diff --git a/src/com/gitblit/LdapUserService.java b/src/com/gitblit/LdapUserService.java
new file mode 100644
index 0000000..3ec45e1
--- /dev/null
+++ b/src/com/gitblit/LdapUserService.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2012 John Crygier
+ * Copyright 2012 gitblit.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.text.MessageFormat;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Set;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ConnectionUtils.BlindSSLSocketFactory;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Implementation of an LDAP user service.
+ * 
+ * @author John Crygier
+ */
+public class LdapUserService extends GitblitUserService {
+
+	public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class);
+	private final String CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
+
+	private IStoredSettings settings;
+
+	public LdapUserService() {
+		super();
+	}
+
+	@Override
+	public void setup(IStoredSettings settings) {
+		this.settings = settings;
+		String file = settings.getString(Keys.realm.ldap_backingUserService, "users.conf");
+		File realmFile = GitBlit.getFileOrFolder(file);
+
+		serviceImpl = createUserService(realmFile);
+		logger.info("LDAP User Service backed by " + serviceImpl.toString());
+	}
+	
+	/**
+	 * Credentials are defined in the LDAP server and can not be manipulated
+	 * from Gitblit.
+	 *
+	 * @return false
+	 * @since 1.0.0
+	 */
+	@Override
+	public boolean supportsCredentialChanges() {
+		return false;
+	}
+	
+	/**
+	 * If the LDAP server will maintain team memberships then LdapUserService
+	 * will not allow team membership changes.  In this scenario all team
+	 * changes must be made on the LDAP server by the LDAP administrator.
+	 * 
+	 * @return true or false
+	 * @since 1.0.0
+	 */	
+	public boolean supportsTeamMembershipChanges() {
+		return !settings.getBoolean(Keys.realm.ldap_maintainTeams, false);
+	}
+
+	/**
+	 * Does the user service support cookie authentication?
+	 * 
+	 * @return true or false
+	 */
+	@Override
+	public boolean supportsCookies() {
+		// TODO cookies need to be reviewed
+		return false;
+	}
+
+	@Override
+	public UserModel authenticate(String username, char[] password) {
+		String domainUser = getDomainUsername(username);		
+		DirContext ctx = getDirContext(domainUser, new String(password));
+		// TODO do we need a bind here?
+		if (ctx != null) {
+			String simpleUsername = getSimpleUsername(username);
+			UserModel user = getUserModel(simpleUsername);
+			if (user == null) {
+				// create user object for new authenticated user
+				user = new UserModel(simpleUsername.toLowerCase());
+			}
+			user.password = new String(password);
+
+			if (!supportsTeamMembershipChanges()) {
+				// Teams are specified in LDAP server
+				// TODO search LDAP for team memberships
+				Set<String> foundTeams = new HashSet<String>();
+				for (String team : foundTeams) {					
+					TeamModel model = getTeamModel(team);
+					if (model == null) {
+						// create the team
+						model = new TeamModel(team.toLowerCase());
+						updateTeamModel(model);
+					}
+					// add team to the user
+					user.teams.add(model);
+				}
+			}
+			
+			try {
+				ctx.close();
+			} catch (NamingException e) {
+				logger.error("Can not close context", e);
+			}
+			return user;
+		}		
+		return null;		
+	}	
+	
+	protected DirContext getDirContext() {
+		String username = settings.getString(Keys.realm.ldap_username, "");
+		String password = settings.getString(Keys.realm.ldap_password, "");
+		return getDirContext(username, password);
+	}
+
+	protected DirContext getDirContext(String username, String password) {
+		try {
+			String server = settings.getRequiredString(Keys.realm.ldap_server);
+			Hashtable<String, String> env = new Hashtable<String, String>();
+			env.put(Context.INITIAL_CONTEXT_FACTORY, CONTEXT_FACTORY);
+			env.put(Context.PROVIDER_URL, server);
+			if (server.startsWith("ldaps:")) {
+				env.put("java.naming.ldap.factory.socket", BlindSSLSocketFactory.class.getName());
+			}
+			// TODO consider making this a setting
+			env.put("com.sun.jndi.ldap.read.timeout", "5000");
+
+			if (!StringUtils.isEmpty(username)) {
+				// authenticated login
+				env.put(Context.SECURITY_AUTHENTICATION, "simple");
+				env.put(Context.SECURITY_PRINCIPAL, getDomainUsername(username));
+				env.put(Context.SECURITY_CREDENTIALS, password == null ? "":password.trim());
+			}
+			return new InitialDirContext(env);
+		} catch (NamingException e) {
+			logger.warn(MessageFormat.format("Error connecting to LDAP with credentials. Please check {0}, {1}, and {2}",
+					Keys.realm.ldap_server, Keys.realm.ldap_username, Keys.realm.ldap_password), e);
+			return null;
+		}
+	}
+	
+	/**
+	 * Returns a simple username without any domain prefixes.
+	 * 
+	 * @param username
+	 * @return a simple username
+	 */
+	protected String getSimpleUsername(String username) {
+		int lastSlash = username.lastIndexOf('\\');
+		if (lastSlash > -1) {
+			username = username.substring(lastSlash + 1);
+		}
+		return username;
+	}
+
+	/**
+	 * Returns a username with a domain prefix as long as the username does not
+	 * already have a comain prefix.
+	 * 
+	 * @param username
+	 * @return a domain username
+	 */
+	protected String getDomainUsername(String username) {
+		String domain = settings.getString(Keys.realm.ldap_domain, null);
+		String domainUsername = username;
+		if (!StringUtils.isEmpty(domain) && (domainUsername.indexOf('\\') == -1)) {
+			domainUsername = domain + "\\" + username;
+		}
+		return domainUsername.trim();
+	}
+}
diff --git a/src/com/gitblit/utils/ConnectionUtils.java b/src/com/gitblit/utils/ConnectionUtils.java
index 9ad62d0..f0b4111 100644
--- a/src/com/gitblit/utils/ConnectionUtils.java
+++ b/src/com/gitblit/utils/ConnectionUtils.java
@@ -16,16 +16,22 @@
 package com.gitblit.utils;
 
 import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
 import java.net.URL;
 import java.net.URLConnection;
+import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
 import java.security.SecureRandom;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 
+import javax.net.SocketFactory;
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.X509TrustManager;
 
@@ -87,6 +93,89 @@
 		}
 		return conn;
 	}
+		
+	// Copyright (C) 2009 The Android Open Source Project
+	//
+	// Licensed under the Apache License, Version 2.0 (the "License");
+	// you may not use this file except in compliance with the License.
+	// You may obtain a copy of the License at
+	//
+	// http://www.apache.org/licenses/LICENSE-2.0
+	//
+	// Unless required by applicable law or agreed to in writing, software
+	// distributed under the License is distributed on an "AS IS" BASIS,
+	// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	// See the License for the specific language governing permissions and
+	// limitations under the License.
+	public static class BlindSSLSocketFactory extends SSLSocketFactory {
+		private static final BlindSSLSocketFactory INSTANCE;
+
+		static {
+			try {
+				final SSLContext context = SSLContext.getInstance("SSL");
+				final TrustManager[] trustManagers = { new DummyTrustManager() };
+				final SecureRandom rng = new SecureRandom();
+				context.init(null, trustManagers, rng);
+				INSTANCE = new BlindSSLSocketFactory(context.getSocketFactory());
+			} catch (GeneralSecurityException e) {
+				throw new RuntimeException("Cannot create BlindSslSocketFactory", e);
+			}
+		}
+
+		public static SocketFactory getDefault() {
+			return INSTANCE;
+		}
+
+		private final SSLSocketFactory sslFactory;
+
+		private BlindSSLSocketFactory(final SSLSocketFactory sslFactory) {
+			this.sslFactory = sslFactory;
+		}
+
+		@Override
+		public Socket createSocket(Socket s, String host, int port, boolean autoClose)
+				throws IOException {
+			return sslFactory.createSocket(s, host, port, autoClose);
+		}
+
+		@Override
+		public String[] getDefaultCipherSuites() {
+			return sslFactory.getDefaultCipherSuites();
+		}
+
+		@Override
+		public String[] getSupportedCipherSuites() {
+			return sslFactory.getSupportedCipherSuites();
+		}
+
+		@Override
+		public Socket createSocket() throws IOException {
+			return sslFactory.createSocket();
+		}
+
+		@Override
+		public Socket createSocket(String host, int port) throws IOException,
+		UnknownHostException {
+			return sslFactory.createSocket(host, port);
+		}
+
+		@Override
+		public Socket createSocket(InetAddress host, int port) throws IOException {
+			return sslFactory.createSocket(host, port);
+		}
+
+		@Override
+		public Socket createSocket(String host, int port, InetAddress localHost,
+				int localPort) throws IOException, UnknownHostException {
+			return sslFactory.createSocket(host, port, localHost, localPort);
+		}
+
+		@Override
+		public Socket createSocket(InetAddress address, int port,
+				InetAddress localAddress, int localPort) throws IOException {
+			return sslFactory.createSocket(address, port, localAddress, localPort);
+		}
+	}
 
 	/**
 	 * DummyTrustManager trusts all certificates.
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties
index e73addc..295db8a 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -270,4 +270,6 @@
 gb.noFederation = Sorry, {0} is not configured to federate with any Gitblit instances.
 gb.proposalFailed = Sorry, {0} did not receive any proposal data!
 gb.proposalError = Sorry, {0} reports that an unexpected error occurred!
-gb.failedToSendProposal = Failed to send proposal!
\ No newline at end of file
+gb.failedToSendProposal = Failed to send proposal!
+gb.userServiceDoesNotPermitAddUser = {0} does not permit adding a user account!
+gb.userServiceDoesNotPermitPasswordChanges = {0} does not permit password changes!
\ No newline at end of file
diff --git a/src/com/gitblit/wicket/pages/BasePage.java b/src/com/gitblit/wicket/pages/BasePage.java
index 3852818..94ed633 100644
--- a/src/com/gitblit/wicket/pages/BasePage.java
+++ b/src/com/gitblit/wicket/pages/BasePage.java
@@ -254,9 +254,11 @@
 				add(new Label("username", GitBlitWebSession.get().getUser().toString() + ":"));
 				add(new LinkPanel("loginLink", null, markupProvider.getString("gb.logout"),
 						LogoutPage.class));
+				boolean editCredentials = GitBlit.self().supportsCredentialChanges();
 				// quick and dirty hack for showing a separator
-				add(new Label("separator", "|"));
-				add(new BookmarkablePageLink<Void>("changePasswordLink", ChangePasswordPage.class));
+				add(new Label("separator", "|").setVisible(editCredentials));
+				add(new BookmarkablePageLink<Void>("changePasswordLink", 
+						ChangePasswordPage.class).setVisible(editCredentials));
 			} else {
 				// login
 				add(new Label("username").setVisible(false));
diff --git a/src/com/gitblit/wicket/pages/ChangePasswordPage.java b/src/com/gitblit/wicket/pages/ChangePasswordPage.java
index 4fb5d23..cbe732f 100644
--- a/src/com/gitblit/wicket/pages/ChangePasswordPage.java
+++ b/src/com/gitblit/wicket/pages/ChangePasswordPage.java
@@ -50,6 +50,12 @@
 			// no authentication enabled
 			throw new RestartResponseException(getApplication().getHomePage());
 		}
+		
+		if (!GitBlit.self().supportsCredentialChanges()) {
+			error(MessageFormat.format(getString("gb.userServiceDoesNotPermitPasswordChanges"),
+					GitBlit.getString(Keys.realm.userService, "users.conf")), true);
+		}
+		
 		setupPage(getString("gb.changePassword"), GitBlitWebSession.get().getUser().username);
 
 		StatelessForm<Void> form = new StatelessForm<Void>("passwordForm") {
diff --git a/src/com/gitblit/wicket/pages/EditTeamPage.java b/src/com/gitblit/wicket/pages/EditTeamPage.java
index 890ea8f..96bd188 100644
--- a/src/com/gitblit/wicket/pages/EditTeamPage.java
+++ b/src/com/gitblit/wicket/pages/EditTeamPage.java
@@ -217,9 +217,12 @@
 		// do not let the browser pre-populate these fields
 		form.add(new SimpleAttributeModifier("autocomplete", "off"));
 
+		// not all user services support manipulating team memberships
+		boolean editMemberships = GitBlit.self().supportsTeamMembershipChanges();
+		
 		// field names reflective match TeamModel fields
 		form.add(new TextField<String>("name"));
-		form.add(users);
+		form.add(users.setEnabled(editMemberships));
 		mailingLists = new Model<String>(teamModel.mailingLists == null ? ""
 				: StringUtils.flattenStrings(teamModel.mailingLists, " "));
 		form.add(new TextField<String>("mailingLists", mailingLists));
diff --git a/src/com/gitblit/wicket/pages/EditUserPage.java b/src/com/gitblit/wicket/pages/EditUserPage.java
index 36f7578..e7b4287 100644
--- a/src/com/gitblit/wicket/pages/EditUserPage.java
+++ b/src/com/gitblit/wicket/pages/EditUserPage.java
@@ -54,6 +54,10 @@
 	public EditUserPage() {
 		// create constructor
 		super();
+		if (!GitBlit.self().supportsCredentialChanges()) {
+			error(MessageFormat.format(getString("gb.userServiceDoesNotPermitAddUser"),
+					GitBlit.getString(Keys.realm.userService, "users.conf")), true);
+		}
 		isCreate = true;
 		setupPage(new UserModel(""));
 	}
@@ -200,20 +204,26 @@
 		
 		// do not let the browser pre-populate these fields
 		form.add(new SimpleAttributeModifier("autocomplete", "off"));
+		
+		// not all user services support manipulating username and password
+		boolean editCredentials = GitBlit.self().supportsCredentialChanges();
+
+		// not all user services support manipulating team memberships
+		boolean editTeams = GitBlit.self().supportsTeamMembershipChanges();
 
 		// field names reflective match UserModel fields
-		form.add(new TextField<String>("username"));
+		form.add(new TextField<String>("username").setEnabled(editCredentials));
 		PasswordTextField passwordField = new PasswordTextField("password");
 		passwordField.setResetPassword(false);
-		form.add(passwordField);
+		form.add(passwordField.setEnabled(editCredentials));
 		PasswordTextField confirmPasswordField = new PasswordTextField("confirmPassword",
 				confirmPassword);
 		confirmPasswordField.setResetPassword(false);
-		form.add(confirmPasswordField);
+		form.add(confirmPasswordField.setEnabled(editCredentials));
 		form.add(new CheckBox("canAdmin"));
 		form.add(new CheckBox("excludeFromFederation"));
 		form.add(repositories);
-		form.add(teams);
+		form.add(teams.setEnabled(editTeams));
 
 		form.add(new Button("save"));
 		Button cancel = new Button("cancel") {
diff --git a/src/com/gitblit/wicket/panels/UsersPanel.java b/src/com/gitblit/wicket/panels/UsersPanel.java
index ad2ed92..4b0edb3 100644
--- a/src/com/gitblit/wicket/panels/UsersPanel.java
+++ b/src/com/gitblit/wicket/panels/UsersPanel.java
@@ -39,7 +39,8 @@
 		super(wicketId);
 
 		Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);
-		adminLinks.add(new BookmarkablePageLink<Void>("newUser", EditUserPage.class));
+		adminLinks.add(new BookmarkablePageLink<Void>("newUser", EditUserPage.class)
+				.setVisible(GitBlit.self().supportsCredentialChanges()));
 		add(adminLinks.setVisible(showAdmin));
 
 		final List<UserModel> users = GitBlit.self().getAllUsers();

--
Gitblit v1.9.1