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 # 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()) { if (!StringUtils.isEmpty(model.password)) { config.setString(USER, model.username, PASSWORD, model.password); } // user roles List<String> roles = new ArrayList<String>(); 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(); 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 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. 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) { 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); } 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. 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 src/com/gitblit/LdapUserService.java
New file @@ -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(); } } 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; @@ -88,6 +94,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. * src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -271,3 +271,5 @@ 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! gb.userServiceDoesNotPermitAddUser = {0} does not permit adding a user account! gb.userServiceDoesNotPermitPasswordChanges = {0} does not permit password changes! 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)); 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") { 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)); 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("")); } @@ -201,19 +205,25 @@ // 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") { 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();