James Moger
2014-03-08 b5361179d924eab162e17d7923f60d91cffb2d08
Extract key manager interface and implement a file-based key manager
3 files added
4 files modified
391 ■■■■ changed files
src/main/distrib/data/gitblit.properties 11 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/FileKeyManager.java 154 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/IKeyManager.java 39 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/NullKeyManager.java 66 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/SshDaemon.java 62 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java 49 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java 10 ●●●● patch | view | raw | blame | history
src/main/distrib/data/gitblit.properties
@@ -110,7 +110,16 @@
# RESTART REQUIRED
git.sshBindInterface = 
# Directory for storing user SSH keys.
# Specify the SSH key manager to use for retrieving, storing, and removing
# SSH keys.
#
# Valid key managers are:
#    com.gitblit.transport.ssh.FileKeyManager
#
# SINCE 1.5.0
git.sshKeysManager = com.gitblit.transport.ssh.FileKeyManager
# Directory for storing user SSH keys when using the FileKeyManager.
#
# SINCE 1.5.0
git.sshKeysFolder= ${baseFolder}/ssh
src/main/java/com/gitblit/transport/ssh/FileKeyManager.java
New file
@@ -0,0 +1,154 @@
/*
 * Copyright 2014 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.transport.ssh;
import java.io.File;
import java.io.IOException;
import java.security.PublicKey;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.codec.binary.Base64;
import org.apache.sshd.common.util.Buffer;
import org.eclipse.jgit.lib.Constants;
import com.gitblit.Keys;
import com.gitblit.manager.IRuntimeManager;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
/**
 * Manages SSH keys on the filesystem.
 *
 * @author James Moger
 *
 */
public class FileKeyManager implements IKeyManager {
    protected final IRuntimeManager runtimeManager;
    public FileKeyManager(IRuntimeManager runtimeManager) {
        this.runtimeManager = runtimeManager;
    }
    @Override
    public String toString() {
        File dir = runtimeManager.getFileOrFolder(Keys.git.sshKeysFolder, "${baseFolder}/ssh");
        return MessageFormat.format("{0} ({1})", getClass().getSimpleName(), dir);
    }
    @Override
    public FileKeyManager start() {
        return this;
    }
    @Override
    public boolean isReady() {
        return true;
    }
    @Override
    public FileKeyManager stop() {
        return this;
    }
    @Override
    public List<PublicKey> getKeys(String username) {
        try {
            File keys = getKeystore(username);
            if (!keys.exists()) {
                return null;
            }
            if (keys.exists()) {
                String str = Files.toString(keys, Charsets.ISO_8859_1);
                String [] entries = str.split("\n");
                List<PublicKey> list = new ArrayList<PublicKey>();
                for (String entry : entries) {
                    if (entry.trim().length() == 0) {
                        // skip blanks
                        continue;
                    }
                    if (entry.charAt(0) == '#') {
                        // skip comments
                        continue;
                    }
                    final String[] parts = entry.split(" ");
                    final byte[] bin = Base64.decodeBase64(Constants.encodeASCII(parts[1]));
                    list.add(new Buffer(bin).getRawPublicKey());
                }
                if (list.isEmpty()) {
                    return null;
                }
                return list;
            }
        } catch (IOException e) {
            throw new RuntimeException("Canot read ssh keys", e);
        }
        return null;
    }
    @Override
    public boolean addKey(String username, String data) {
        try {
            File keys = getKeystore(username);
            Files.append(data + '\n', keys, Charsets.ISO_8859_1);
            return true;
        } catch (IOException e) {
            throw new RuntimeException("Cannot add ssh key", e);
        }
    }
    @Override
    public boolean removeKey(String username, String data) {
        try {
            File keystore = getKeystore(username);
            if (keystore.exists()) {
                String str = Files.toString(keystore, Charsets.ISO_8859_1);
                List<String> keep = new ArrayList<String>();
                String [] entries = str.split("\n");
                for (String entry : entries) {
                    if (entry.trim().length() == 0) {
                        // keep blanks
                        keep.add(entry);
                        continue;
                    }
                    if (entry.charAt(0) == '#') {
                        // keep comments
                        keep.add(entry);
                        continue;
                    }
                    final String[] parts = entry.split(" ");
                    if (!parts[1].equals(data)) {
                        keep.add(entry);
                    }
                }
                return true;
            }
        } catch (IOException e) {
            throw new RuntimeException("Cannot remove ssh key", e);
        }
        return false;
    }
    protected File getKeystore(String username) {
        File dir = runtimeManager.getFileOrFolder(Keys.git.sshKeysFolder, "${baseFolder}/ssh");
        dir.mkdirs();
        File keys = new File(dir, username + ".keys");
        return keys;
    }
}
src/main/java/com/gitblit/transport/ssh/IKeyManager.java
New file
@@ -0,0 +1,39 @@
/*
 * Copyright 2014 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.transport.ssh;
import java.security.PublicKey;
import java.util.List;
/**
 *
 * @author James Moger
 *
 */
public interface IKeyManager {
    IKeyManager start();
    boolean isReady();
    IKeyManager stop();
    List<PublicKey> getKeys(String username);
    boolean addKey(String username, String data);
    boolean removeKey(String username, String data);
}
src/main/java/com/gitblit/transport/ssh/NullKeyManager.java
New file
@@ -0,0 +1,66 @@
/*
 * Copyright 2014 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.transport.ssh;
import java.security.PublicKey;
import java.util.List;
/**
 * Rejects all SSH key management requests.
 *
 * @author James Moger
 *
 */
public class NullKeyManager implements IKeyManager {
    public NullKeyManager() {
    }
    @Override
    public String toString() {
        return getClass().getSimpleName();
    }
    @Override
    public NullKeyManager start() {
        return this;
    }
    @Override
    public boolean isReady() {
        return true;
    }
    @Override
    public NullKeyManager stop() {
        return this;
    }
    @Override
    public List<PublicKey> getKeys(String username) {
        return null;
    }
    @Override
    public boolean addKey(String username, String data) {
        return false;
    }
    @Override
    public boolean removeKey(String username, String data) {
        return false;
    }
}
src/main/java/com/gitblit/transport/ssh/SshDaemon.java
@@ -21,6 +21,8 @@
import java.text.MessageFormat;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.inject.Singleton;
import org.apache.sshd.SshServer;
import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider;
import org.eclipse.jgit.internal.JGitText;
@@ -41,6 +43,10 @@
import com.gitblit.utils.IdGenerator;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.WorkQueue;
import dagger.Module;
import dagger.ObjectGraph;
import dagger.Provides;
/**
 * Manager for the ssh transport. Roughly analogous to the
@@ -65,9 +71,9 @@
    private final AtomicBoolean run;
    @SuppressWarnings("unused")
    private final IGitblit gitblit;
    private final SshServer sshd;
    private final ObjectGraph injector;
    /**
     * Construct the Gitblit SSH daemon.
@@ -76,12 +82,15 @@
     */
    public SshDaemon(IGitblit gitblit, IdGenerator idGenerator) {
        this.gitblit = gitblit;
        this.injector = ObjectGraph.create(new SshModule());
        IStoredSettings settings = gitblit.getSettings();
        int port = settings.getInteger(Keys.git.sshPort, 0);
        String bindInterface = settings.getString(Keys.git.sshBindInterface,
                "localhost");
        IKeyManager keyManager = getKeyManager();
        InetSocketAddress addr;
        if (StringUtils.isEmpty(bindInterface)) {
            addr = new InetSocketAddress(port);
@@ -94,7 +103,7 @@
        sshd.setHost(addr.getHostName());
        sshd.setKeyPairProvider(new PEMGeneratorHostKeyProvider(new File(
                gitblit.getBaseFolder(), HOST_KEY_STORE).getPath()));
        sshd.setPublickeyAuthenticator(new SshKeyAuthenticator(gitblit));
        sshd.setPublickeyAuthenticator(new SshKeyAuthenticator(keyManager, gitblit));
        sshd.setPasswordAuthenticator(new SshPasswordAuthenticator(gitblit));
        sshd.setSessionFactory(new SshSessionFactory(idGenerator));
        sshd.setFileSystemFactory(new DisabledFilesystemFactory());
@@ -176,4 +185,51 @@
            }
        }
    }
    protected IKeyManager getKeyManager() {
        IKeyManager keyManager = null;
        IStoredSettings settings = gitblit.getSettings();
        String clazz = settings.getString(Keys.git.sshKeysManager, FileKeyManager.class.getName());
        if (StringUtils.isEmpty(clazz)) {
            clazz = FileKeyManager.class.getName();
        }
        try {
            Class<? extends IKeyManager> managerClass = (Class<? extends IKeyManager>) Class.forName(clazz);
            keyManager = injector.get(managerClass).start();
            if (keyManager.isReady()) {
                log.info("{} is ready.", keyManager);
            } else {
                log.warn("{} is disabled.", keyManager);
            }
        } catch (Exception e) {
            log.error("failed to create ssh key manager " + clazz, e);
            keyManager = injector.get(NullKeyManager.class).start();
        }
        return keyManager;
    }
    /**
     * A nested Dagger graph is used for constructor dependency injection of
     * complex classes.
     *
     * @author James Moger
     *
     */
    @Module(
            library = true,
            injects = {
                    NullKeyManager.class,
                    FileKeyManager.class
            }
            )
    class SshModule {
        @Provides @Singleton NullKeyManager provideNullKeyManager() {
            return new NullKeyManager();
        }
        @Provides @Singleton FileKeyManager provideFileKeyManager() {
            return new FileKeyManager(SshDaemon.this.gitblit);
        }
    }
}
src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java
@@ -15,29 +15,20 @@
 */
package com.gitblit.transport.ssh;
import java.io.File;
import java.io.IOException;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.apache.commons.codec.binary.Base64;
import org.apache.sshd.common.util.Buffer;
import org.apache.sshd.server.PublickeyAuthenticator;
import org.apache.sshd.server.session.ServerSession;
import org.eclipse.jgit.lib.Constants;
import com.gitblit.Keys;
import com.gitblit.manager.IGitblit;
import com.gitblit.manager.IAuthenticationManager;
import com.gitblit.models.UserModel;
import com.google.common.base.Charsets;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.io.Files;
/**
 *
@@ -46,7 +37,9 @@
 */
public class SshKeyAuthenticator implements PublickeyAuthenticator {
    protected final IGitblit gitblit;
    protected final IKeyManager keyManager;
    protected final IAuthenticationManager authManager;
    LoadingCache<String, List<PublicKey>> sshKeyCache = CacheBuilder
            .newBuilder().
@@ -54,37 +47,13 @@
            maximumSize(100)
            .build(new CacheLoader<String, List<PublicKey>>() {
                public List<PublicKey> load(String username) {
                    try {
                        File dir = gitblit.getFileOrFolder(Keys.git.sshKeysFolder, "${baseFolder}/ssh");
                        dir.mkdirs();
                        File keys = new File(dir, username + ".keys");
                        if (!keys.exists()) {
                            return null;
                        }
                        if (keys.exists()) {
                            String str = Files.toString(keys, Charsets.ISO_8859_1);
                            String [] entries = str.split("\n");
                            List<PublicKey> list = new ArrayList<PublicKey>();
                            for (String entry : entries) {
                                final String[] parts = entry.split(" ");
                                final byte[] bin = Base64.decodeBase64(Constants.encodeASCII(parts[1]));
                                list.add(new Buffer(bin).getRawPublicKey());
                            }
                            if (list.isEmpty()) {
                                return null;
                            }
                            return list;
                        }
                    } catch (IOException e) {
                        throw new RuntimeException("Canot read public key", e);
                    }
                    return null;
                    return keyManager.getKeys(username);
                }
            });
    public SshKeyAuthenticator(IGitblit gitblit) {
        this.gitblit = gitblit;
    public SshKeyAuthenticator(IKeyManager keyManager, IAuthenticationManager authManager) {
        this.keyManager = keyManager;
        this.authManager = authManager;
    }
    @Override
@@ -115,7 +84,7 @@
        // now that the key has been validated, check with the authentication
        // manager to ensure that this user exists and can authenticate
        sd.authenticationSuccess(username);
        UserModel user = gitblit.authenticate(sd);
        UserModel user = authManager.authenticate(sd);
        if (user != null) {
            return true;
        }
src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java
@@ -20,7 +20,7 @@
import org.apache.sshd.server.PasswordAuthenticator;
import org.apache.sshd.server.session.ServerSession;
import com.gitblit.manager.IGitblit;
import com.gitblit.manager.IAuthenticationManager;
import com.gitblit.models.UserModel;
/**
@@ -30,16 +30,16 @@
 */
public class SshPasswordAuthenticator implements PasswordAuthenticator {
    protected final IGitblit gitblit;
    protected final IAuthenticationManager authManager;
    public SshPasswordAuthenticator(IGitblit gitblit) {
        this.gitblit = gitblit;
    public SshPasswordAuthenticator(IAuthenticationManager authManager) {
        this.authManager = authManager;
    }
    @Override
    public boolean authenticate(String username, String password, ServerSession session) {
        username = username.toLowerCase(Locale.US);
        UserModel user = gitblit.authenticate(username, password.toCharArray());
        UserModel user = authManager.authenticate(username, password.toCharArray());
        if (user != null) {
            SshSession sd = session.getAttribute(SshSession.KEY);
            sd.authenticationSuccess(username);