diff options
7 files changed, 342 insertions, 49 deletions
diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties index bb16b6a5..2338cc5b 100644 --- a/src/main/distrib/data/gitblit.properties +++ b/src/main/distrib/data/gitblit.properties @@ -110,7 +110,16 @@ git.sshPort = 29418 # 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
diff --git a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java new file mode 100644 index 00000000..87912cae --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java @@ -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; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/IKeyManager.java b/src/main/java/com/gitblit/transport/ssh/IKeyManager.java new file mode 100644 index 00000000..8b94fd60 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/IKeyManager.java @@ -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); +} diff --git a/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java b/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java new file mode 100644 index 00000000..2a2ef364 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java @@ -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; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index cc938bc1..de57f5ff 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -21,6 +21,8 @@ import java.net.InetSocketAddress; 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; @@ -42,6 +44,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 * {@link com.gitblit.transport.git.GitDaemon} class. @@ -65,9 +71,9 @@ public class SshDaemon { 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 class SshDaemon { */ 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 @@ public class SshDaemon { 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 @@ public class SshDaemon { } } } + + 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); + } + } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java index 4cda268e..d41afdd0 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java +++ b/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 @@ import com.google.common.io.Files; */ 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 @@ public class SshKeyAuthenticator implements PublickeyAuthenticator { 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 @@ public class SshKeyAuthenticator implements PublickeyAuthenticator { // 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; } diff --git a/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java index e39b5f72..ce01df76 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java @@ -20,7 +20,7 @@ import java.util.Locale; 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 @@ import com.gitblit.models.UserModel; */ 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); |