~jan0sch/smederee
Showing details for patch ff0c2faef514bb68f8b3a611ca463b69532e296d.
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