~jan0sch/smederee

Showing details for patch ff0c2faef514bb68f8b3a611ca463b69532e296d.
2023-05-31 (Wed), 6:16 PM - Jens Grassel - ff0c2faef514bb68f8b3a611ca463b69532e296d

Add basic SFTP support when operating via SSH.

- provide a sftp subsystem for the mina sshd server
- implement a custom `SftpFileSystemAccessor` to check permissions
- increase cloning speed about 10 times :-)
Summary of changes
1 files added
  • modules/hub/src/main/scala/de/smederee/ssh/DarcsSftpFileSystemAccessor.scala
1 files modified with 6 lines added and 2 lines removed
  • modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala with 6 added and 2 removed lines
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSftpFileSystemAccessor.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSftpFileSystemAccessor.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSftpFileSystemAccessor.scala	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSftpFileSystemAccessor.scala	2025-01-16 05:15:45.488128933 +0000
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2022  Contributors as noted in the AUTHORS.md file
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.smederee.ssh
+
+import java.nio.file._
+
+import cats._
+import cats.effect._
+import cats.effect.std.Dispatcher
+import cats.effect.unsafe.implicits.global
+import cats.syntax.all._
+import de.smederee.hub.config._
+import de.smederee.hub.VcsRepositoryName
+import de.smederee.security.{ UserId, Username }
+import org.apache.sshd.sftp.server._
+import org.slf4j.LoggerFactory
+
+import scala.util.matching.Regex
+
+/** An access layer for the sftp subsystem to enable permission checks and further logic.
+  *
+  * @param darcsConfiguration
+  *   The configuration needed to properly execute underlying darcs commands and access repository data on the
+  *   filesystem.
+  * @param repository
+  *   A repository providing the needed functionality like getting ssh keys and user/repo information.
+  */
+final class DarcsSftpFileSystemAccessor(
+    darcsConfiguration: DarcsConfiguration,
+    repository: SshAuthenticationRepository[IO]
+) extends SftpFileSystemAccessor {
+  private val log = LoggerFactory.getLogger(classOf[DarcsSftpFileSystemAccessor])
+
+  /** Check if the given repository of the given owner is readable by the user with the given id.
+    *
+    * @param ownerName
+    *   The unique name (user name) of the owner of the repository.
+    * @param repoName
+    *   The name of the repository which is unique within the context of the owner.
+    * @param userId
+    *   The unique id of the account that is requesting access.
+    * @return
+    *   Either `true` if the repository is readable by the user or `false` otherwise.
+    */
+  protected def repositoryIsReadableBy(ownerName: Username, repoName: VcsRepositoryName, userId: UserId): Boolean =
+    Dispatcher
+      .sequential[IO]
+      .use { dispatcher =>
+        val checkPermissions =
+          for {
+            _        <- IO.delay(log.debug(s"Checking if vcs repository $ownerName/$repoName is readable by $userId."))
+            vcsOwner <- repository.findVcsRepositoryOwner(ownerName)
+            _        <- IO.delay(log.debug(s"VCS repository owner name maps to $vcsOwner."))
+            userIsOwner = vcsOwner.exists(_.uid === userId)
+          } yield userIsOwner
+        checkPermissions.recoverWith { error =>
+          log.error("Internal Server Error", error)
+          false.pure[IO]
+        }
+      }
+      .unsafeRunSync()
+
+  @SuppressWarnings(Array("scalafix:DisableSyntax.throw"))
+  override def resolveLocalFilePath(subsystem: SftpSubsystemProxy, rootDir: Path, remotePath: String): Path = {
+    // FIXME: This works but is pretty clumsy/noisy. Find a way to kill the connection on the first illegal access.
+    val sshKeyOwnerId = subsystem.getServerSession().getAttribute(SshServerConfiguration.SshKeyOwnerIdAttribute)
+    val accessIsPermitted = remotePath match {
+      case DarcsSftpFileSystemAccessor.ExtractRepositoryOwnerAndName(owner, repository, path) =>
+        log.debug("SFTP permission check for {} on {}/{} ({})", sshKeyOwnerId, owner, repository, path)
+        (SshUsername.from(owner), VcsRepositoryName.from(repository)).mapN { case (owner, repository) =>
+          repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId) && !path.contains("..")
+        }
+      case noMatch =>
+        log.error("SFTP permission check regex did not match for: {} ({})", remotePath, noMatch)
+        false.some
+    }
+    accessIsPermitted.filter(_ === true) match {
+      case Some(true) => super.resolveLocalFilePath(subsystem, rootDir, remotePath)
+      case _ => throw new InvalidPathException(remotePath, "You are only allowed to access your own repositories!")
+    }
+  }
+
+}
+
+object DarcsSftpFileSystemAccessor {
+  val ExtractRepositoryOwnerAndName: Regex = "^([a-z][a-z0-9]{1,31})/([a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63})(.*)".r
+}
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala	2025-01-16 05:15:45.488128933 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala	2025-01-16 05:15:45.488128933 +0000
@@ -18,6 +18,7 @@
 package de.smederee.ssh
 
 import java.nio.file.*
+import java.util.Collections
 
 import cats._
 import cats.effect._
@@ -29,6 +30,7 @@
 import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory
 import org.apache.sshd.server.SshServer
 import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
+import org.apache.sshd.sftp.server._
 import pureconfig._
 
 import scala.util.matching.Regex
@@ -152,9 +154,11 @@
     server.setFileSystemFactory(
       new VirtualFileSystemFactory(darcsConfiguration.repositoriesDirectory.toPath)
     )
+    // Add our custom sftp subsystem to provide more performant access for darcs operations.
+    val sftpFileSystemAccessor = new DarcsSftpFileSystemAccessor(darcsConfiguration, repository)
+    val sftpSubsystem = new SftpSubsystemFactory.Builder().withFileSystemAccessor(sftpFileSystemAccessor).build()
+    server.setSubsystemFactories(Collections.singletonList(sftpSubsystem))
     // Add our custom command factory which must provide darcs and scp functionality.
-    // val sftpSubsystem = new SftpSubsystemFactory.Builder().build()
-    // server.setSubsystemFactories(Collections.singletonList(sftpSubsystem))
     val darcsCommand = new DarcsSshCommandFactory(darcsConfiguration, repository)
     server.setCommandFactory(darcsCommand)
     server