naujoks-stefan opened a new issue, #534: URL: https://github.com/apache/mina-sshd/issues/534
### Version 1.7.0 ### Bug description Hello everyone, I'm working on a project where I need to set up an SFTP server using Apache SSHD, which allows users to access multiple directories within a specified root directory. The root directory should be restricted to C:/FTP, and users should only be able to see and access their designated subdirectories within this root directory. Despite setting the C:/FTP as the root directory and specifying the user directories, users are seeing the entire C:/ drive instead of being restricted to C:/FTP and their specific directories. ### Actual behavior Current Setup I have implemented a custom SFTP server using Apache SSHD, and it successfully authenticates users. However, I'm facing issues with restricting the user's view to their specific directories. Instead of seeing their designated directories within C:/FTP, users can see the entire C:/ drive. Here's the code I am using: public class MySftpServer { private static final Log log = LogFactory.getLog(MySftpServer.class); private static final AttributeKey<UserManager.User> USER_KEY = new AttributeKey<>(); @PostConstruct public void startServer() throws IOException { start(); } private void start() throws IOException { SshServer sshd = SshServer.setUpDefaultServer(); sshd.setHost("localhost"); sshd.setPort(2222); sshd.setKeyPairProvider(new PEMKeyPairProvider("src/main/resources/host_key.ser")); sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); sshd.setPasswordAuthenticator(new PasswordAuthenticator() { @Override public boolean authenticate(String username, String password, ServerSession session) { UserManager.User user = UserManager.authenticate(username, password); if (user != null) { session.setAttribute(USER_KEY, user); return true; } return false; } }); sshd.setPublickeyAuthenticator(new AuthorizedKeysAuthenticator(Paths.get("src/main/resources/authorized_keys"))); // Set idle timeout to 30 minutes sshd.getProperties().put(SshServer.IDLE_TIMEOUT, TimeUnit.MINUTES.toMillis(30)); // Set authentication timeout to 2 minutes sshd.getProperties().put(SshServer.AUTH_TIMEOUT, TimeUnit.MINUTES.toMillis(2)); // Set up the custom file system factory with user home directories sshd.setFileSystemFactory(new CustomFileSystemFactory(Paths.get("C:/FTP"))); sshd.start(); log.info("SFTP server started"); } public static class CustomFileSystemFactory implements FileSystemFactory { private final Path rootDir; public CustomFileSystemFactory(Path rootDir) { this.rootDir = rootDir; } @Override public FileSystem createFileSystem(Session session) throws IOException { UserManager.User user = session.getAttribute(USER_KEY); if (user == null) { throw new IOException("No user found in session"); } List<Path> userDirs = user.getDirectories().stream() .map(dir -> rootDir.resolve(dir).toAbsolutePath().normalize()) .collect(Collectors.toList()); return new MultiDirectoryFileSystem(userDirs); } } public static class MultiDirectoryFileSystem extends FileSystem { private final List<Path> roots; public MultiDirectoryFileSystem(List<Path> directories) { this.roots = directories; } @Override public FileSystemProvider provider() { return new MultiDirectoryFileSystemProvider(roots); } @Override public void close() throws IOException { // Implement close logic if needed } @Override public boolean isOpen() { return true; } @Override public boolean isReadOnly() { return false; } @Override public String getSeparator() { return FileSystems.getDefault().getSeparator(); } @Override public Iterable<Path> getRootDirectories() { return roots; } @Override public Iterable<FileStore> getFileStores() { return Collections.emptyList(); } @Override public Set<String> supportedFileAttributeViews() { return FileSystems.getDefault().supportedFileAttributeViews(); } @Override public Path getPath(String first, String... more) { Path path = FileSystems.getDefault().getPath(first, more).toAbsolutePath().normalize(); for (Path root : roots) { if (path.startsWith(root)) { return path; } } throw new IllegalArgumentException("Path is not under any root: " + path); } @Override public PathMatcher getPathMatcher(String syntaxAndPattern) { return FileSystems.getDefault().getPathMatcher(syntaxAndPattern); } @Override public UserPrincipalLookupService getUserPrincipalLookupService() { return FileSystems.getDefault().getUserPrincipalLookupService(); } @Override public WatchService newWatchService() throws IOException { return FileSystems.getDefault().newWatchService(); } } public static class MultiDirectoryFileSystemProvider extends FileSystemProvider { private final List<Path> roots; public MultiDirectoryFileSystemProvider(List<Path> roots) { this.roots = roots; } @Override public String getScheme() { return "multi-dir"; } @Override public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException { throw new UnsupportedOperationException("Creating new file systems is not supported."); } @Override public FileSystem getFileSystem(URI uri) { throw new UnsupportedOperationException("Getting file system by URI is not supported."); } @Override public Path getPath(URI uri) { return Paths.get(uri).toAbsolutePath().normalize(); } @Override public FileChannel newFileChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException { validateAccess(path); return FileSystems.getDefault().provider().newFileChannel(path, options, attrs); } @Override public AsynchronousFileChannel newAsynchronousFileChannel(Path path, Set<? extends OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) throws IOException { validateAccess(path); return FileSystems.getDefault().provider().newAsynchronousFileChannel(path, options, executor, attrs); } @Override public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException { validateAccess(dir); return FileSystems.getDefault().provider().newDirectoryStream(dir, filter); } @Override public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException { validateAccess(dir); FileSystems.getDefault().provider().createDirectory(dir, attrs); } @Override public void delete(Path path) throws IOException { validateAccess(path); FileSystems.getDefault().provider().delete(path); } @Override public void copy(Path source, Path target, CopyOption... options) throws IOException { validateAccess(source); validateAccess(target); FileSystems.getDefault().provider().copy(source, target, options); } @Override public void move(Path source, Path target, CopyOption... options) throws IOException { validateAccess(source); validateAccess(target); FileSystems.getDefault().provider().move(source, target, options); } @Override public boolean isSameFile(Path path, Path path2) throws IOException { validateAccess(path); validateAccess(path2); return FileSystems.getDefault().provider().isSameFile(path, path2); } @Override public boolean isHidden(Path path) throws IOException { validateAccess(path); return FileSystems.getDefault().provider().isHidden(path); } @Override public FileStore getFileStore(Path path) throws IOException { validateAccess(path); return FileSystems.getDefault().provider().getFileStore(path); } @Override public void checkAccess(Path path, AccessMode... modes) throws IOException { validateAccess(path); FileSystems.getDefault().provider().checkAccess(path, modes); } @Override public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) { try { validateAccess(path); } catch (IOException e) { e.printStackTrace(); } return FileSystems.getDefault().provider().getFileAttributeView(path, type, options); } @Override public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException { validateAccess(path); return FileSystems.getDefault().provider().readAttributes(path, type, options); } @Override public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException { validateAccess(path); return FileSystems.getDefault().provider().readAttributes(path, attributes, options); } @Override public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { validateAccess(path); FileSystems.getDefault().provider().setAttribute(path, attribute, value, options); } private void validateAccess(Path path) throws IOException { for (Path root : roots) { if (path.startsWith(root)) { return; } } throw new IOException("Access denied to path: " + path); } @Override public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException { validateAccess(path); return FileSystems.getDefault().provider().newByteChannel(path, options, attrs); } } public static class PEMKeyPairProvider implements KeyPairProvider { private final KeyPair keyPair; public PEMKeyPairProvider(String privateKeyPath) throws IOException { try (PEMParser pemParser = new PEMParser(new FileReader(privateKeyPath))) { JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); Object object = pemParser.readObject(); if (object instanceof PEMKeyPair) { this.keyPair = converter.getKeyPair((PEMKeyPair) object); } else if (object instanceof PrivateKeyInfo) { PrivateKey privateKey = converter.getPrivateKey((PrivateKeyInfo) object); SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemParser.readObject()); PublicKey publicKey = converter.getPublicKey(publicKeyInfo); this.keyPair = new KeyPair(publicKey, privateKey); } else { throw new IllegalArgumentException("Invalid key format"); } } } @Override public Iterable<KeyPair> loadKeys() { return Collections.singletonList(keyPair); } } } ### Expected behavior The SFTP root directory should be C:/FTP. Users should only see and access their designated directories within C:/FTP. For example, user1 should only see C:/FTP/001 and C:/FTP/002. ### Relevant log output _No response_ ### Other information _No response_ -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: dev-unsubscr...@mina.apache.org.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@mina.apache.org For additional commands, e-mail: dev-h...@mina.apache.org