From ae8d5bff1d43f81a629a2b92895c32ab28182c3f Mon Sep 17 00:00:00 2001 From: Justin Crawford Date: Sat, 23 Nov 2019 01:49:05 -0800 Subject: [PATCH] Implemented access control for SFTP --- pom.xml | 6 + .../sshd/ConfigPasswordAuthenticator.java | 2 +- .../java/com/ryanmichela/sshd/SshdPlugin.java | 123 ++++++++++++++++-- src/main/resources/config.yml | 33 ++++- 4 files changed, 153 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index 5a44018..2d9ad9c 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,12 @@ 2.3.0 + + org.apache.sshd + sshd-contrib + 2.3.0 + + org.apache.sshd sshd-common diff --git a/src/main/java/com/ryanmichela/sshd/ConfigPasswordAuthenticator.java b/src/main/java/com/ryanmichela/sshd/ConfigPasswordAuthenticator.java index d054204..51566d6 100644 --- a/src/main/java/com/ryanmichela/sshd/ConfigPasswordAuthenticator.java +++ b/src/main/java/com/ryanmichela/sshd/ConfigPasswordAuthenticator.java @@ -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. 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) SshdPlugin.instance.getLogger().warning("Config has no such user: " + username); diff --git a/src/main/java/com/ryanmichela/sshd/SshdPlugin.java b/src/main/java/com/ryanmichela/sshd/SshdPlugin.java index d82da58..b822e5e 100644 --- a/src/main/java/com/ryanmichela/sshd/SshdPlugin.java +++ b/src/main/java/com/ryanmichela/sshd/SshdPlugin.java @@ -1,9 +1,13 @@ package com.ryanmichela.sshd; 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.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.server.session.ServerSession; 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 com.ryanmichela.sshd.ConsoleShellFactory; @@ -14,18 +18,35 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; import java.util.logging.Level; +import java.util.stream.Stream; /** * Copyright 2013 Ryan Michela */ -public -class SshdPlugin extends JavaPlugin +public class SshdPlugin extends JavaPlugin { + private SshServer sshd; + public static SshdPlugin instance; - private SshServer sshd; - public static SshdPlugin instance; + public static List GetSections(ConfigurationSection source) + { + if (source == null) + return null; + + List nodes = new ArrayList(); + for (String key : source.getKeys(false)) + { + if (source.isConfigurationSection(key)) + nodes.add(source.getConfigurationSection(key)); + } + return nodes; + } @Override public void onLoad() { @@ -73,9 +94,90 @@ class SshdPlugin extends JavaPlugin if (getConfig().getBoolean("EnableSFTP")) { - sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); - sshd.setFileSystemFactory( - new VirtualFileSystemFactory(FileSystems.getDefault().getPath(getDataFolder().getAbsolutePath()).getParent().getParent())); + // Handle access control for SFTP. + SftpSubsystemFactory.Builder builder = new SftpSubsystemFactory.Builder(); + 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 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 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()); @@ -95,11 +197,16 @@ class SshdPlugin extends JavaPlugin { 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) { // do nothing + e.printStackTrace(); } } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 5c3e115..69bf66a 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -37,5 +37,34 @@ PasswordType: bcrypt # Associate each username with a password hash (or the password if the PasswordType is set to PLAIN) Credentials: -# user1: password1 -# user2: password2 + # Username (should match SSH key if using key-based authentication) + 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