Implemented access control for SFTP

This commit is contained in:
Justin Crawford 2019-11-23 01:49:05 -08:00
parent 3ad2a810f8
commit ae8d5bff1d
No known key found for this signature in database
GPG Key ID: 0D84DEDBB8EF259C
4 changed files with 153 additions and 11 deletions

View File

@ -53,6 +53,12 @@
<version>2.3.0</version> <version>2.3.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-contrib</artifactId>
<version>2.3.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.sshd</groupId> <groupId>org.apache.sshd</groupId>
<artifactId>sshd-common</artifactId> <artifactId>sshd-common</artifactId>

View File

@ -20,7 +20,7 @@ public class ConfigPasswordAuthenticator implements PasswordAuthenticator {
{ {
// Depending on our hash type, we have to try and figure out what we're doing. // Depending on our hash type, we have to try and figure out what we're doing.
String HashType = SshdPlugin.instance.getConfig().getString("PasswordType"); String HashType = SshdPlugin.instance.getConfig().getString("PasswordType");
String ConfigHash = SshdPlugin.instance.getConfig().getString("Credentials." + username.trim()); String ConfigHash = SshdPlugin.instance.getConfig().getString("Credentials." + username.trim() + ".password");
if (ConfigHash == null) if (ConfigHash == null)
SshdPlugin.instance.getLogger().warning("Config has no such user: " + username); SshdPlugin.instance.getLogger().warning("Config has no such user: " + username);

View File

@ -1,9 +1,13 @@
package com.ryanmichela.sshd; package com.ryanmichela.sshd;
import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
import org.apache.sshd.common.session.helpers.AbstractSession;
import org.apache.sshd.server.SshServer; import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
import org.apache.sshd.server.subsystem.sftp.SimpleAccessControlSftpEventListener;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import com.ryanmichela.sshd.ConsoleShellFactory; import com.ryanmichela.sshd.ConsoleShellFactory;
@ -14,18 +18,35 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.FileSystems; import java.nio.file.FileSystems;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Stream;
/** /**
* Copyright 2013 Ryan Michela * Copyright 2013 Ryan Michela
*/ */
public public class SshdPlugin extends JavaPlugin
class SshdPlugin extends JavaPlugin
{ {
private SshServer sshd;
public static SshdPlugin instance;
private SshServer sshd; public static List<ConfigurationSection> GetSections(ConfigurationSection source)
public static SshdPlugin instance; {
if (source == null)
return null;
List<ConfigurationSection> nodes = new ArrayList<ConfigurationSection>();
for (String key : source.getKeys(false))
{
if (source.isConfigurationSection(key))
nodes.add(source.getConfigurationSection(key));
}
return nodes;
}
@Override public void onLoad() @Override public void onLoad()
{ {
@ -73,9 +94,90 @@ class SshdPlugin extends JavaPlugin
if (getConfig().getBoolean("EnableSFTP")) if (getConfig().getBoolean("EnableSFTP"))
{ {
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); // Handle access control for SFTP.
sshd.setFileSystemFactory( SftpSubsystemFactory.Builder builder = new SftpSubsystemFactory.Builder();
new VirtualFileSystemFactory(FileSystems.getDefault().getPath(getDataFolder().getAbsolutePath()).getParent().getParent())); builder.addSftpEventListener(new SimpleAccessControlSftpEventListener() {
protected boolean isAccessAllowed(ServerSession session, String remote, Path localpath)
{
try
{
ConfigurationSection UsernameNamespace = getConfig().getConfigurationSection("Credentials." + session.getUsername() + ".sftp");
// They don't have SFTP enabled so deny them.
if (UsernameNamespace == null || !UsernameNamespace.getBoolean("enabled"))
return false;
List<ConfigurationSection> rules = GetSections(UsernameNamespace.getConfigurationSection("rules"));
if (rules != null)
{
for (ConfigurationSection path : rules)
{
// Check if the requesting path matches
if (localpath.toString().matches(path.getName()))
{
// Check if they have read permissions
if (path.getBoolean("readable"))
return true;
getLogger().info(String.format("Denied %s read access to \"%s\" matching rule \"%s\"", session.getUsername(), localpath.toString(), path.getName()));
return false;
}
}
}
return UsernameNamespace.getString("default").equalsIgnoreCase("allow");
}
catch (Exception e)
{
e.printStackTrace();
// Automatically deny.
return false;
}
}
protected boolean isModificationAllowed(ServerSession session, String remote, Path localpath)
{
try
{
ConfigurationSection UsernameNamespace = getConfig().getConfigurationSection("Credentials." + session.getUsername() + ".sftp");
// They don't have SFTP enabled so deny them.
if (UsernameNamespace == null || !UsernameNamespace.getBoolean("enabled"))
return false;
// Check a list of files against a path trying to be accessed.
List<ConfigurationSection> rules = GetSections(UsernameNamespace.getConfigurationSection("rules"));
if (rules != null)
{
for (ConfigurationSection path : rules)
{
// Check if the requesting path matches
if (localpath.toString().matches(path.getName()))
{
// Check if they have read permissions
if (path.getBoolean("writeable"))
return true;
getLogger().info(String.format("Denied %s modifications to \"%s\" matching rule \"%s\"", session.getUsername(), localpath.toString(), path.getName()));
return false;
}
}
}
return UsernameNamespace.getString("default").equalsIgnoreCase("allow");
}
catch (Exception e)
{
e.printStackTrace();
// Automatically deny.
return false;
}
}
});
sshd.setSubsystemFactories(Collections.singletonList(builder.build()));
sshd.setFileSystemFactory(new VirtualFileSystemFactory(FileSystems.getDefault().getPath(getDataFolder().getAbsolutePath()).getParent().getParent()));
} }
this.getCommand("mkpasswd").setExecutor(new MkpasswdCommand()); this.getCommand("mkpasswd").setExecutor(new MkpasswdCommand());
@ -95,11 +197,16 @@ class SshdPlugin extends JavaPlugin
{ {
try try
{ {
sshd.stop(); // Terminate any active sessions
for (AbstractSession as : sshd.getActiveSessions())
as.close(true);
// Pass "true" to stop immediately!
sshd.stop(true);
} }
catch (Exception e) catch (Exception e)
{ {
// do nothing // do nothing
e.printStackTrace();
} }
} }
} }

View File

@ -37,5 +37,34 @@ PasswordType: bcrypt
# Associate each username with a password hash (or the password if the PasswordType is set to PLAIN) # Associate each username with a password hash (or the password if the PasswordType is set to PLAIN)
Credentials: Credentials:
# user1: password1 # Username (should match SSH key if using key-based authentication)
# user2: password2 justasic:
# Password hash from /mkpasswd command
password: $2a$10$Oqk83FrypRrMF35EDeoQDuidJOQEWBE0joEQ7MJFi/Oeg26wQ3fm2
# Whether they can read, write, or have read/write permissions to console.
console: RW
# SFTP access for this user.
sftp:
# Whether SFTP is enabled for this user.
enabled: true
# Whether to deny by default or allow by default
default: allow
# Rules regarding their SFTP access.
# These rules are relative to the server root.
# This acts as a chroot for the server root.
# Each path can be an absolute path or a regular expression.
rules:
"/path/to/file":
# Whether the user can read the file over SFTP
readable: true
# Whether the user can write/modify the file over SFTP
writeable: true
"/path/to/regex/*":
readable: true
writeable: false
"/path/to/directory/":
readable: false
writeable: true
"/another/example/path":
readable: false
writeable: false