James Moger
2012-04-06 6cca8699f98a606ff19e88d40a8a2535fdc340e7
Skeleton LdapUserService based on John Cryiger's implementation
1 files added
14 files modified
517 ■■■■■ changed files
distrib/gitblit.properties 46 ●●●●● patch | view | raw | blame | history
src/com/gitblit/ConfigUserService.java 25 ●●●●● patch | view | raw | blame | history
src/com/gitblit/FileUserService.java 27 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitBlit.java 16 ●●●●● patch | view | raw | blame | history
src/com/gitblit/GitblitUserService.java 38 ●●●●● patch | view | raw | blame | history
src/com/gitblit/IStoredSettings.java 18 ●●●●● patch | view | raw | blame | history
src/com/gitblit/IUserService.java 16 ●●●●● patch | view | raw | blame | history
src/com/gitblit/LdapUserService.java 200 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/ConnectionUtils.java 89 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.properties 4 ●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/BasePage.java 6 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/ChangePasswordPage.java 6 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditTeamPage.java 5 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/pages/EditUserPage.java 18 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/UsersPanel.java 3 ●●●● patch | view | raw | blame | history
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()) {
            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>();
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
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) {
        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);
    }
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;
@@ -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.
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!
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(""));
    }
@@ -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") {
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();