~jan0sch/smederee

Showing details for patch 41c3bb902743192030102be19ba7e6e3223b143b.
2022-10-31 (Mon), 4:22 PM - Jens Grassel - 41c3bb902743192030102be19ba7e6e3223b143b

SSH: Authentication

This adds the foundation for authentication via ssh and public keys.

Currently the username must match our generic one (darcs) and then we try
to find a key with the same fingerprint like the provided one in our 
database. The found key is converted into the `AuthorizedKeyEntry` format
that mina sshd understands and the further authentication delegated to the
`PublickeyAuthenticator` of mina sshd.

The authorisation check inside the `DarcsSshCommandFactory` is currently
done simply by checking if the requested repository is owned by the user
owning the ssh key. So for now users can only work via ssh on their own
repositories.

*HEADS UP*
The availability of `scp` functionality (needed for darcs) implies that
we need to tighten security in that section.
Currently we simply check the scp path for relative paths and deny access.
Summary of changes
1 files added
  • modules/hub/src/main/scala/de/smederee/ssh/DoobieSshAuthenticationRepository.scala
7 files modified with 245 lines added and 59 lines removed
  • modules/hub/src/it/scala/de/smederee/ssh/SshServerProviderTest.scala with 1 added and 1 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/HubServer.scala with 2 added and 1 removed lines
  • modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala with 100 added and 35 removed lines
  • modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala with 27 added and 1 removed lines
  • modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticationRepository.scala with 5 added and 7 removed lines
  • modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticator.scala with 84 added and 4 removed lines
  • modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala with 26 added and 10 removed lines
diff -rN -u old-smederee/modules/hub/src/it/scala/de/smederee/ssh/SshServerProviderTest.scala new-smederee/modules/hub/src/it/scala/de/smederee/ssh/SshServerProviderTest.scala
--- old-smederee/modules/hub/src/it/scala/de/smederee/ssh/SshServerProviderTest.scala	2025-02-01 21:56:19.655641519 +0000
+++ new-smederee/modules/hub/src/it/scala/de/smederee/ssh/SshServerProviderTest.scala	2025-02-01 21:56:19.655641519 +0000
@@ -61,7 +61,7 @@
           port = portNumber,
           serverKeyFile = keyfile
         )
-        val provider = new SshServerProvider(darcsConfig, sshConfig)
+        val provider = new SshServerProvider(darcsConfig, configuration.database, sshConfig)
         provider.run().use { server =>
           assert(server.isStarted(), "Server not started!")
           assertEquals(server.getPort(), portNumber.toString.toInt)
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala	2025-02-01 21:56:19.655641519 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala	2025-02-01 21:56:19.655641519 +0000
@@ -148,7 +148,8 @@
       // Create our ssh server fiber (or a dummy one if disabled).
       sshServerProvider = configuration.service.ssh.enabled match {
         case false => None
-        case true  => Option(new SshServerProvider(configuration.service.darcs, configuration.service.ssh))
+        case true =>
+          Option(new SshServerProvider(configuration.service.darcs, configuration.database, configuration.service.ssh))
       }
       sshServer = sshServerProvider.fold(IO.unit.as(ExitCode.Success))(
         _.run().use(server =>
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala	2025-02-01 21:56:19.655641519 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala	2025-02-01 21:56:19.655641519 +0000
@@ -20,9 +20,13 @@
 import java.io.{ File, InputStream, OutputStream }
 import java.nio.file.Paths
 
+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.{ Username, VcsRepositoryName }
+import de.smederee.hub.{ UserId, Username, VcsRepositoryName }
 import org.apache.sshd.scp.common.ScpHelper
 import org.apache.sshd.scp.server._
 import org.apache.sshd.server.channel.ChannelSession
@@ -190,12 +194,39 @@
   * @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 DarcsSshCommandFactory(darcsConfiguration: DarcsConfiguration) extends CommandFactory {
+final class DarcsSshCommandFactory(darcsConfiguration: DarcsConfiguration, repository: SshAuthenticationRepository[IO])
+    extends CommandFactory {
   private val log = LoggerFactory.getLogger(classOf[DarcsSshCommandFactory])
 
+  /** 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[IO]
+      .use { dispatcher =>
+        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
+      }
+      .unsafeRunSync()
+
   override def createCommand(channel: ChannelSession, command: String): Command = {
     log.debug(s"Requested SSH command: $command")
+    val sshKeyOwnerId = channel.getSession().getAttribute(SshServerConfiguration.SshKeyOwnerIdAttribute)
 
     command match {
       case DarcsSshCommandFactory.FilterDarcsApplyCommand(
@@ -208,15 +239,21 @@
             owner,
             repository
           ) =>
-        (SshUsername.from(owner), VcsRepositoryName.from(repository))
-          .mapN((owner, repository) =>
-            new DarcsApply(
-              darcsConfiguration,
-              owner.toUsername,
-              repository,
-              debugFlag === "--debug"
-            )
-          )
+        (
+          SshUsername.from(owner),
+          VcsRepositoryName.from(repository)
+        )
+          .mapN { case (owner, repository) =>
+            if (repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId))
+              new DarcsApply(
+                darcsConfiguration,
+                owner.toUsername,
+                repository,
+                debugFlag === "--debug"
+              )
+            else
+              new UnknownCommand("You are only allowed to access your own repositories!")
+          }
           .getOrElse(new UnknownCommand(command))
       case DarcsSshCommandFactory.FilterDarcsTransferModeCommand(
             "transfer-mode",
@@ -224,32 +261,58 @@
             owner,
             repository
           ) =>
-        (SshUsername.from(owner), VcsRepositoryName.from(repository))
-          .mapN((owner, repository) =>
-            // new DarcsTransferMode(darcsConfiguration, owner.toUsername, repository)
-            new UnknownCommand(command) // FIXME Make transfer-mode work (stalls currently).
-          )
+        (
+          SshUsername.from(owner),
+          VcsRepositoryName.from(repository)
+        )
+          .mapN { case (owner, repository) =>
+            if (repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId))
+              // new DarcsTransferMode(darcsConfiguration, owner.toUsername, repository)
+              new UnknownCommand(command) // FIXME Make transfer-mode work (stalls currently).
+            else
+              new UnknownCommand("You are only allowed to access your own repositories!")
+          }
           .getOrElse(new UnknownCommand(command))
-      case DarcsSshCommandFactory.FilterScpCommand("-f", _, "--", path) =>
-        new ScpCommand(
-          channel,
-          command,
-          null,
-          ScpHelper.DEFAULT_SEND_BUFFER_SIZE,
-          ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE,
-          null,
-          null
+      case DarcsSshCommandFactory.FilterScpCommand("-f", ignored, "--", owner, repository, path) =>
+        (
+          SshUsername.from(owner),
+          VcsRepositoryName.from(repository)
         )
-      case DarcsSshCommandFactory.FilterScpCommand("-f", null, null, path) =>
-        new ScpCommand(
-          channel,
-          command,
-          null,
-          ScpHelper.DEFAULT_SEND_BUFFER_SIZE,
-          ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE,
-          null,
-          null
+          .mapN { case (owner, repository) =>
+            if (repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId) && !path.contains(".."))
+              new ScpCommand(
+                channel,
+                command,
+                null,
+                ScpHelper.DEFAULT_SEND_BUFFER_SIZE,
+                ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE,
+                null,
+                null
+              )
+            else
+              new UnknownCommand("You are only allowed to access your own repositories!")
+          }
+          .getOrElse(new UnknownCommand(command))
+      case DarcsSshCommandFactory.FilterScpCommand("-f", null, null, owner, repository, path) =>
+        (
+          SshUsername.from(owner),
+          VcsRepositoryName.from(repository)
         )
+          .mapN { case (owner, repository) =>
+            if (repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId) && !path.contains(".."))
+              new ScpCommand(
+                channel,
+                command,
+                null,
+                ScpHelper.DEFAULT_SEND_BUFFER_SIZE,
+                ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE,
+                null,
+                null
+              )
+            else
+              new UnknownCommand("You are only allowed to access your own repositories!")
+          }
+          .getOrElse(new UnknownCommand(command))
       case _ => new UnknownCommand(command)
     }
   }
@@ -262,6 +325,8 @@
   val FilterDarcsTransferModeCommand: Regex =
     "^darcs (transfer-mode) (--repodir) ([a-z][a-z0-9]{1,31})/([a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63})/?$".r
   // After the initial transfer-mode call darcs will fallback to using scp.
-  val FilterScpCommand: Regex = "^scp (-f) ((--)\\s)?([^\u0000]+)$".r
+  // val FilterScpCommand: Regex = "^scp (-f) ((--)\\s)?([^\u0000]+)$".r
+  val FilterScpCommand: Regex =
+    "^scp (-f) ((--)\\s)?([a-z][a-z0-9]{1,31})/([a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63})/([^\u0000]+)$".r
   // TODO Add support for sftp because that could be enforced from the client side.
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DoobieSshAuthenticationRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DoobieSshAuthenticationRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DoobieSshAuthenticationRepository.scala	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DoobieSshAuthenticationRepository.scala	2025-02-01 21:56:19.655641519 +0000
@@ -0,0 +1,56 @@
+/*
+ * 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.util.UUID
+
+import cats.effect._
+import de.smederee.hub._
+import doobie._
+import doobie.Fragments._
+import doobie.implicits._
+import doobie.postgres.implicits._
+import org.slf4j.LoggerFactory
+
+final class DoobieSshAuthenticationRepository[F[_]: Sync](tx: Transactor[F]) extends SshAuthenticationRepository[F] {
+  private final val log = LoggerFactory.getLogger(getClass)
+
+  given Meta[EncodedKeyBytes]   = Meta[String].timap(EncodedKeyBytes.unsafeFrom)(_.toString)
+  given Meta[KeyComment]        = Meta[String].timap(KeyComment.apply)(_.toString)
+  given Meta[KeyFingerprint]    = Meta[String].timap(KeyFingerprint.apply)(_.toString)
+  given Meta[SshKeyType]        = Meta[String].timap(id => SshKeyType.Mappings(id))(_.identifier)
+  given Meta[UserId]            = Meta[UUID].timap(UserId.apply)(_.toUUID)
+  given Meta[Username]          = Meta[String].timap(Username.apply)(_.toString)
+  given Meta[VcsRepositoryName] = Meta[String].timap(VcsRepositoryName.apply)(_.toString)
+
+  override def findSshKey(fingerprint: KeyFingerprint): F[Option[PublicSshKey]] =
+    sql"""SELECT id, uid, key_type, key, fingerprint, comment, created_at, last_used_at FROM "ssh_keys" WHERE fingerprint = $fingerprint"""
+      .query[PublicSshKey]
+      .option
+      .transact(tx)
+
+  override def findVcsRepositoryOwner(name: Username): F[Option[VcsRepositoryOwner]] =
+    sql"""SELECT uid, name FROM "accounts" WHERE name = $name LIMIT 1"""
+      .query[VcsRepositoryOwner]
+      .option
+      .transact(tx)
+
+  override def updateLastUsed(keyId: UUID): F[Int] =
+    sql"""UPDATE "ssh_keys" SET last_used_at = NOW() WHERE id = $keyId""".update.run.transact(tx)
+
+}
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala	2025-02-01 21:56:19.655641519 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala	2025-02-01 21:56:19.655641519 +0000
@@ -24,6 +24,7 @@
 import cats._
 import cats.syntax.all._
 import de.smederee.hub._
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry
 import org.bouncycastle.crypto.util.OpenSSHPublicKeyUtil
 
 import scala.util.Try
@@ -257,7 +258,7 @@
       val rawFingerprint = Try {
         val publicKey   = OpenSSHPublicKeyUtil.parsePublicKey(base64Key.toByteArray)
         val digestedKey = digest.digest(OpenSSHPublicKeyUtil.encodePublicKey(publicKey))
-        Base64.getEncoder().encodeToString(digestedKey).reverse.dropWhile(_ == '=').reverse // Remove padding (`=`)
+        Base64.getEncoder().withoutPadding().encodeToString(digestedKey)
       }.toOption
       rawFingerprint.flatMap(KeyFingerprint.from)
     }
@@ -266,3 +267,28 @@
     }
   }
 }
+
+extension (sshKey: PublicSshKey) {
+
+  /** Convert this key into an instance of a [[SshPublicKeyStringhPublicKeyString]].
+    *
+    * @return
+    *   A well formatted ssh public key string.
+    */
+  def toSshPublicKeyString: SshPublicKeyString =
+    SshPublicKeyString(
+      s"""${sshKey.keyType.identifier} ${sshKey.keyBytes.toString}${sshKey.comment
+          .map(c => s" ${c.toString}")
+          .getOrElse("")}"""
+    )
+
+  /** Convert this key into an instance of a AuthorizedKeyEntry useable by the Apache Mina SSHD library.
+    *
+    * @return
+    *   Either the converted AuthorizedKeyEntry or an error message.
+    */
+  def toAuthorizedKeyEntry: Either[String, AuthorizedKeyEntry] =
+    Try {
+      AuthorizedKeyEntry.parseAuthorizedKeyEntry(sshKey.toSshPublicKeyString.toString)
+    }.toEither.leftMap(_.getMessage())
+}
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticationRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticationRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticationRepository.scala	2025-02-01 21:56:19.655641519 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticationRepository.scala	2025-02-01 21:56:19.655641519 +0000
@@ -45,16 +45,14 @@
     */
   def findSshKey(fingerprint: KeyFingerprint): F[Option[PublicSshKey]]
 
-  /** Search for the vcs repository entry with the given owner and name.
+  /** Search for a repository owner of whom we only know the name.
     *
-    * @param ownerName
-    *   The name of the repository owner.
-    * @param repoName
-    *   The repository name which must be unique in regard to the owner.
+    * @param name
+    *   The name of the repository owner which is the username of the actual owners account.
     * @return
-    *   An option to the successfully found vcs repository entry.
+    *   An option to successfully found repository owner.
     */
-  def findVcsRepository(ownerName: Username, repoName: VcsRepositoryName): F[Option[VcsRepository]]
+  def findVcsRepositoryOwner(name: Username): F[Option[VcsRepositoryOwner]]
 
   /** Update the last used column for the key in the database.
     *
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticator.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticator.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticator.scala	2025-02-01 21:56:19.655641519 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticator.scala	2025-02-01 21:56:19.655641519 +0000
@@ -19,11 +19,21 @@
 
 import java.security.PublicKey
 
+import cats._
+import cats.effect._
+import cats.effect.std.Dispatcher
+import cats.effect.unsafe.implicits.global
+import cats.syntax.all._
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry
+import org.apache.sshd.common.config.keys.KeyUtils
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver
 import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator
 import org.apache.sshd.server.session.ServerSession
-import org.apache.sshd.common.AttributeRepository
 import org.slf4j.LoggerFactory
 
+import scala.jdk.CollectionConverters._
+import scala.util.Try
+
 /** A custom PublickeyAuthenticator implementation for restricting access via ssh.
   *
   * Currently we follow this flow:
@@ -38,10 +48,80 @@
   *
   * @param genericUser
   *   A name which represents a ssh user name that can be used for generic access.
+  * @param repository
+  *   A repository providing the needed functionality like getting ssh keys and user/repo information.
   */
-final class SshAuthenticator(genericUser: SshUsername) extends PublickeyAuthenticator {
+final class SshAuthenticator(
+    genericUser: SshUsername,
+    repository: SshAuthenticationRepository[IO]
+) extends PublickeyAuthenticator {
   private val log = LoggerFactory.getLogger(getClass)
 
-  override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = true // FIXME
+  override def authenticate(providedUsername: String, providedPublicKey: PublicKey, session: ServerSession): Boolean =
+    Dispatcher[IO]
+      .use { dispatcher =>
+        val check = for {
+          _ <- IO.delay(log.debug(s"Authentication request for $providedUsername from ${session.getRemoteAddress()}."))
+          convertedName   <- IO.delay(SshUsername.from(providedUsername))
+          usernameIsValid <- IO.delay(convertedName.exists(_ === genericUser))
+          keyIsValid <-
+            if (usernameIsValid)
+              for {
+                fingerprint <- IO.delay(
+                  KeyFingerprint.from(KeyUtils.getFingerPrint(providedPublicKey).drop(7))
+                ) // drop the "SHA256:"
+                _ <- IO.delay(
+                  fingerprint.map(fingerprint =>
+                    log.debug(s"User provided ${providedPublicKey.getAlgorithm()} key with fingerprint: $fingerprint.")
+                  )
+                )
+                providedKeyBytes <- IO.delay(providedPublicKey.getEncoded())
+                possibleUserKey <- fingerprint match {
+                  case None              => IO.pure(None)
+                  case Some(fingerprint) => repository.findSshKey(fingerprint)
+                }
+                _ <- possibleUserKey match {
+                  case None =>
+                    IO.delay(log.warn(s"No ssh key found in the database for given fingerprint: $fingerprint!"))
+                  case Some(userKey) =>
+                    IO.delay(log.debug(s"Found matching ${userKey.keyType} key (owner: ${userKey.ownerId})."))
+                }
+                keyIsValid <- possibleUserKey match {
+                  case Some(userKey) =>
+                    // We create a `AuthorizedKeyEntry` from our stored key and delegate the key validation to mina sshd.
+                    // Also we store the owner id of the key in the server session for later usage.
+                    IO.delay {
+                      userKey.toAuthorizedKeyEntry match {
+                        case Left(error) =>
+                          log.error(s"Could not convert user key ${userKey.fingerprint} to AuthorizedKeyEntry: $error")
+                          false
+                        case Right(authKey) =>
+                          session.setAttribute(SshServerConfiguration.SshKeyOwnerIdAttribute, userKey.ownerId)
+                          val authenticator = PublickeyAuthenticator.fromAuthorizedEntries(
+                            userKey.fingerprint.toString,
+                            session,
+                            List(authKey).asJava,
+                            PublicKeyEntryResolver.IGNORING
+                          )
+                          authenticator.authenticate(providedUsername, providedPublicKey, session)
+                      }
+                    }
+                  case _ => IO.pure(false)
+                }
+                _ <- IO.delay {
+                  if (keyIsValid)
+                    log.debug("Keys are matching!")
+                  else
+                    log.info("Stored key and provided one are not matching!")
+                }
+              } yield keyIsValid
+            else
+              for {
+                _ <- IO.delay(log.debug(s"Provided username $providedUsername was not accepted!"))
+              } yield false
+        } yield usernameIsValid && keyIsValid
+        check
+      }
+      .unsafeRunSync()
 
 }
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-02-01 21:56:19.655641519 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala	2025-02-01 21:56:19.655641519 +0000
@@ -20,19 +20,22 @@
 import java.nio.file.*
 import java.util.Collections
 
-import cats.*
-import cats.data.*
-import cats.effect.*
-import cats.syntax.all.*
-import com.comcast.ip4s.*
-import de.smederee.hub.Username
+import cats._
+import cats.data._
+import cats.effect._
+import cats.effect.std.Dispatcher
+import cats.syntax.all._
+import com.comcast.ip4s._
+import de.smederee.hub.{ UserId, Username }
 import de.smederee.hub.config._
+import doobie._
+import org.apache.sshd.common.AttributeRepository.AttributeKey
 import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory
 import org.apache.sshd.scp.server.ScpCommandFactory
 import org.apache.sshd.server.SshServer
 import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
 import org.apache.sshd.sftp.server.SftpSubsystemFactory
-import pureconfig.*
+import pureconfig._
 
 import scala.util.matching.Regex
 
@@ -98,6 +101,9 @@
   // The default configuration key under which to lookup the ssh server configuration.
   final val parentKey: ConfigKey = ConfigKey("ssh")
 
+  // A key marker for storing the owner id of an ssh-key in the ssh server session.
+  final val SshKeyOwnerIdAttribute: AttributeKey[UserId] = new AttributeKey[UserId]()
+
   given Eq[SshServerConfiguration] = Eq.fromUniversalEquals
 
   given ConfigReader[Host]        = ConfigReader.fromStringOpt[Host](Host.fromString)
@@ -116,11 +122,14 @@
   * @param darcsConfiguration
   *   The configuration needed to properly execute underlying darcs commands and access repository data on the
   *   filesystem.
+  * @param databaseConfiguration
+  *   The configuration needed to access the database.
   * @param sshConfiguration
   *   The configuration with all values needed to properly configure the underlying Apache sshd server.
   */
 final class SshServerProvider(
     darcsConfiguration: DarcsConfiguration,
+    databaseConfiguration: DatabaseConfig,
     sshConfiguration: SshServerConfiguration
 ) {
 
@@ -130,14 +139,21 @@
     *   An instance of a Apache MINA SSHD server which is not yet started.
     */
   private def createServer(): SshServer = {
-    val server = SshServer.setUpDefaultServer()
+    val transactor = Transactor.fromDriverManager[IO](
+      databaseConfiguration.driver,
+      databaseConfiguration.url,
+      databaseConfiguration.user,
+      databaseConfiguration.pass
+    )
+    val repository = new DoobieSshAuthenticationRepository[IO](transactor)
+    val server     = SshServer.setUpDefaultServer()
     server.setHost(sshConfiguration.host.toString)
     server.setPort(sshConfiguration.port.toString.toInt)
     val keyProvider = new SimpleGeneratorHostKeyProvider(sshConfiguration.serverKeyFile)
     keyProvider.setAlgorithm("EC")
     keyProvider.setOverwriteAllowed(false)
     server.setKeyPairProvider(keyProvider)
-    server.setPublickeyAuthenticator(new SshAuthenticator(sshConfiguration.genericUser))
+    server.setPublickeyAuthenticator(new SshAuthenticator(sshConfiguration.genericUser, repository))
     server.setShellFactory(
       new NoLogin(sshConfiguration.genericUser, sshConfiguration.host, sshConfiguration.port)
     )
@@ -147,7 +163,7 @@
     // 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)
+    val darcsCommand = new DarcsSshCommandFactory(darcsConfiguration, repository)
     server.setCommandFactory(darcsCommand)
     server
   }