diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..397d9cf7 --- /dev/null +++ b/flake.nix @@ -0,0 +1,37 @@ +{ + inputs = { + typelevel-nix.url = "github:typelevel/typelevel-nix"; + nixpkgs.follows = "typelevel-nix/nixpkgs"; + flake-utils.follows = "typelevel-nix/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils, typelevel-nix }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs-x86_64 = import nixpkgs { + system = "x86_64-darwin"; + }; + scala-cli-overlay = final: prev: { + scala-cli = pkgs-x86_64.scala-cli; + }; + pkgs = import nixpkgs { + inherit system; + overlays = [ typelevel-nix.overlay scala-cli-overlay]; + }; + in + { + devShell = pkgs.devshell.mkShell { + imports = [ typelevel-nix.typelevelShell ]; + packages = [ + pkgs.nodePackages.vscode-langservers-extracted + pkgs.nodePackages.prettier + pkgs.websocat + ]; + typelevelShell = { + nodejs.enable = true; + jdk.package = pkgs.jdk17; + }; + }; + } + ); +} diff --git a/modules/model/shared/src/main/scala/navigate/model/security/package.scala b/modules/model/shared/src/main/scala/navigate/model/security/package.scala deleted file mode 100644 index 9145209e..00000000 --- a/modules/model/shared/src/main/scala/navigate/model/security/package.scala +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) -// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause - -package navigate.model - -import cats.Eq -import io.circe.Decoder -import io.circe.Encoder - -package security { - // Shared classes used for authentication - case class UserLoginRequest(username: String, password: String) derives Decoder - - object UserLoginRequest { - given Eq[UserLoginRequest] = Eq.by(x => (x.username, x.password)) - } - - case class UserDetails(username: String, displayName: String) derives Encoder.AsObject - - object UserDetails { - // Some useful type aliases for user elements - type UID = String - type DisplayName = String - type Groups = List[String] - type Thumbnail = Array[Byte] - - given Eq[UserDetails] = Eq.by(x => (x.username, x.displayName)) - } - -} diff --git a/modules/model/shared/src/main/scala/navigate/model/Distance.scala b/modules/model/src/main/scala/navigate/model/Distance.scala similarity index 100% rename from modules/model/shared/src/main/scala/navigate/model/Distance.scala rename to modules/model/src/main/scala/navigate/model/Distance.scala diff --git a/modules/model/shared/src/main/scala/navigate/model/EngineInputEvent.scala b/modules/model/src/main/scala/navigate/model/EngineInputEvent.scala similarity index 100% rename from modules/model/shared/src/main/scala/navigate/model/EngineInputEvent.scala rename to modules/model/src/main/scala/navigate/model/EngineInputEvent.scala diff --git a/modules/model/shared/src/main/scala/navigate/model/NavigateCommand.scala b/modules/model/src/main/scala/navigate/model/NavigateCommand.scala similarity index 100% rename from modules/model/shared/src/main/scala/navigate/model/NavigateCommand.scala rename to modules/model/src/main/scala/navigate/model/NavigateCommand.scala diff --git a/modules/model/shared/src/main/scala/navigate/model/NavigateEvent.scala b/modules/model/src/main/scala/navigate/model/NavigateEvent.scala similarity index 100% rename from modules/model/shared/src/main/scala/navigate/model/NavigateEvent.scala rename to modules/model/src/main/scala/navigate/model/NavigateEvent.scala diff --git a/modules/model/jvm/src/main/scala/navigate/model/config/AuthenticationConfig.scala b/modules/model/src/main/scala/navigate/model/config/AuthenticationConfig.scala similarity index 100% rename from modules/model/jvm/src/main/scala/navigate/model/config/AuthenticationConfig.scala rename to modules/model/src/main/scala/navigate/model/config/AuthenticationConfig.scala diff --git a/modules/model/jvm/src/main/scala/navigate/model/config/ControlStrategy.scala b/modules/model/src/main/scala/navigate/model/config/ControlStrategy.scala similarity index 100% rename from modules/model/jvm/src/main/scala/navigate/model/config/ControlStrategy.scala rename to modules/model/src/main/scala/navigate/model/config/ControlStrategy.scala diff --git a/modules/model/jvm/src/main/scala/navigate/model/config/Mode.scala b/modules/model/src/main/scala/navigate/model/config/Mode.scala similarity index 100% rename from modules/model/jvm/src/main/scala/navigate/model/config/Mode.scala rename to modules/model/src/main/scala/navigate/model/config/Mode.scala diff --git a/modules/model/jvm/src/main/scala/navigate/model/config/NavigateConfiguration.scala b/modules/model/src/main/scala/navigate/model/config/NavigateConfiguration.scala similarity index 100% rename from modules/model/jvm/src/main/scala/navigate/model/config/NavigateConfiguration.scala rename to modules/model/src/main/scala/navigate/model/config/NavigateConfiguration.scala diff --git a/modules/model/jvm/src/main/scala/navigate/model/config/NavigateEngineConfiguration.scala b/modules/model/src/main/scala/navigate/model/config/NavigateEngineConfiguration.scala similarity index 100% rename from modules/model/jvm/src/main/scala/navigate/model/config/NavigateEngineConfiguration.scala rename to modules/model/src/main/scala/navigate/model/config/NavigateEngineConfiguration.scala diff --git a/modules/model/jvm/src/main/scala/navigate/model/config/SystemsControlConfiguration.scala b/modules/model/src/main/scala/navigate/model/config/SystemsControlConfiguration.scala similarity index 100% rename from modules/model/jvm/src/main/scala/navigate/model/config/SystemsControlConfiguration.scala rename to modules/model/src/main/scala/navigate/model/config/SystemsControlConfiguration.scala diff --git a/modules/model/jvm/src/main/scala/navigate/model/config/WebServerConfiguration.scala b/modules/model/src/main/scala/navigate/model/config/WebServerConfiguration.scala similarity index 100% rename from modules/model/jvm/src/main/scala/navigate/model/config/WebServerConfiguration.scala rename to modules/model/src/main/scala/navigate/model/config/WebServerConfiguration.scala diff --git a/modules/model/shared/src/main/scala/navigate/model/enums/DomeMode.scala b/modules/model/src/main/scala/navigate/model/enums/DomeMode.scala similarity index 100% rename from modules/model/shared/src/main/scala/navigate/model/enums/DomeMode.scala rename to modules/model/src/main/scala/navigate/model/enums/DomeMode.scala diff --git a/modules/model/shared/src/main/scala/navigate/model/enums/M1Source.scala b/modules/model/src/main/scala/navigate/model/enums/M1Source.scala similarity index 100% rename from modules/model/shared/src/main/scala/navigate/model/enums/M1Source.scala rename to modules/model/src/main/scala/navigate/model/enums/M1Source.scala diff --git a/modules/model/shared/src/main/scala/navigate/model/enums/ServerLogLevel.scala b/modules/model/src/main/scala/navigate/model/enums/ServerLogLevel.scala similarity index 100% rename from modules/model/shared/src/main/scala/navigate/model/enums/ServerLogLevel.scala rename to modules/model/src/main/scala/navigate/model/enums/ServerLogLevel.scala diff --git a/modules/model/shared/src/main/scala/navigate/model/enums/ShutterMode.scala b/modules/model/src/main/scala/navigate/model/enums/ShutterMode.scala similarity index 100% rename from modules/model/shared/src/main/scala/navigate/model/enums/ShutterMode.scala rename to modules/model/src/main/scala/navigate/model/enums/ShutterMode.scala diff --git a/modules/model/shared/src/main/scala/navigate/model/enums/TipTiltSource.scala b/modules/model/src/main/scala/navigate/model/enums/TipTiltSource.scala similarity index 100% rename from modules/model/shared/src/main/scala/navigate/model/enums/TipTiltSource.scala rename to modules/model/src/main/scala/navigate/model/enums/TipTiltSource.scala diff --git a/modules/model/shared/src/main/scala/navigate/model/package.scala b/modules/model/src/main/scala/navigate/model/package.scala similarity index 100% rename from modules/model/shared/src/main/scala/navigate/model/package.scala rename to modules/model/src/main/scala/navigate/model/package.scala diff --git a/modules/web/server/src/main/scala/navigate/web/server/security/FreeLDAPAuthenticationService.scala b/modules/web/server/src/main/scala/navigate/web/server/security/FreeLDAPAuthenticationService.scala deleted file mode 100644 index f0998331..00000000 --- a/modules/web/server/src/main/scala/navigate/web/server/security/FreeLDAPAuthenticationService.scala +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) -// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause - -package navigate.web.server.security - -import cats.* -import cats.effect.* -import cats.free.Free -import cats.syntax.all.* -import com.unboundid.ldap.sdk.* -import navigate.model.security.UserDetails -import org.typelevel.log4cats.Logger - -import AuthenticationService.AuthResult - -/** - * Definition of LDAP as a free monad algebra/interpreters - */ -object FreeLDAPAuthenticationService { - import LdapConnectionOps._ - import UserDetails._ - - sealed trait LdapOp[A] - // Operations on ldap - object LdapOp { - // Operation to authenticate a user, It returns true if it works - case class AuthenticateOp(username: String, password: String) extends LdapOp[UID] - // Read the user display name - case class UserDisplayNameOp(uid: UID) extends LdapOp[DisplayName] - // Reads the name, groups and thumbnail - case class DisplayNameGrpThumbOp(uid: UID) - extends LdapOp[(DisplayName, Groups, Option[Thumbnail])] - } - - // Free monad over the free functor of LdapOp. - type LdapM[A] = Free[LdapOp, A] - - // Smart constructors for LdapOp[A] - def bind(u: String, p: String): LdapM[UID] = Free.liftF(LdapOp.AuthenticateOp(u, p)) - def displayName(u: UID): LdapM[DisplayName] = Free.liftF(LdapOp.UserDisplayNameOp(u)) - def nameGroupsThumb(u: UID): LdapM[(DisplayName, Groups, Option[Thumbnail])] = - Free.liftF(LdapOp.DisplayNameGrpThumbOp(u)) - - // Natural transformation to IO - def toF[F[_]: Sync](c: LDAPConnection): LdapOp ~> F = - new (LdapOp ~> F) { - def apply[A](fa: LdapOp[A]): F[A] = - fa match { - case LdapOp.AuthenticateOp(u, p) => Sync[F].delay(c.authenticate(u, p)) - case LdapOp.UserDisplayNameOp(uid) => Sync[F].delay(c.displayName(uid)) - case LdapOp.DisplayNameGrpThumbOp(uid) => Sync[F].delay(c.nameGroupsThumb(uid)) - } - } - - // Run on IO - def runF[F[_]: Sync, A](a: LdapM[A], c: LDAPConnection): F[A] = - a.foldMap(toF(c)) - - // Programs - // Does simple user authentication - def authenticate(u: String, p: String): LdapM[UID] = bind(u, p) - - // Authenticate and reads the display name - def authenticationAndName(u: String, p: String): LdapM[UserDetails] = for { - u <- bind(u, p) - d <- displayName(u) - } yield UserDetails(u, d) - - // Authenticate and reads the name, groups and photo - def authNameGroupThumb(u: String, p: String): LdapM[(UserDetails, Groups, Option[Thumbnail])] = - for { - u <- bind(u, p) - d <- nameGroupsThumb(u) - } yield (UserDetails(u, d._1), d._2, d._3) -} - -/** - * Handles authentication against the AD/LDAP server - */ -class FreeLDAPAuthenticationService[F[_]: Sync: Logger](hosts: List[(String, Int)]) - extends AuthService[F] { - import FreeLDAPAuthenticationService._ - private given Eq[ResultCode] = Eq.fromUniversalEquals - - // Shorten the default timeout - private val Timeout = 1000 - private val Domain = "@gemini.edu" - - lazy val ldapOptions: LDAPConnectionOptions = { - val opts = new LDAPConnectionOptions() - opts.setConnectTimeoutMillis(Timeout) - opts - } - - // Will attempt several servers in case they fail - lazy val failoverServerSet = - new FailoverServerSet(hosts.map(_._1).toArray, hosts.map(_._2).toArray, ldapOptions) - - override def authenticateUser(username: String, password: String): F[AuthResult] = { - // We should always return the domain - val usernameWithDomain = if (username.endsWith(Domain)) username else s"$username$Domain" - - val rsrc = - for { - c <- Resource.make(Sync[F].delay(failoverServerSet.getConnection))(c => - Sync[F].delay(c.close()) - ) - x <- Resource.eval(runF(authenticationAndName(usernameWithDomain, password), c).attempt) - } yield x - - rsrc.use { - case Left(e: LDAPException) if e.getResultCode === ResultCode.NO_SUCH_OBJECT => - Logger[F].error(e)(s"Exception connection to LDAP server: ${e.getExceptionMessage}") *> - BadCredentials(username).asLeft[UserDetails].pure[F].widen[AuthResult] - case Left(e: LDAPException) if e.getResultCode === ResultCode.INVALID_CREDENTIALS => - Logger[F].error(e)(s"Exception connection to LDAP server: ${e.getExceptionMessage}") *> - UserNotFound(username).asLeft[UserDetails].pure[F].widen[AuthResult] - case Left(e: LDAPException) => - Logger[F].error(e)(s"Exception connection to LDAP server: ${e.getExceptionMessage}") *> - GenericFailure("LDAP Authentication error").asLeft[UserDetails].pure[F].widen[AuthResult] - case Left(e: Throwable) => - GenericFailure(e.getMessage).asLeft[UserDetails].pure[F].widen[AuthResult] - case Right(u) => - u.asRight.pure[F] - } - } - -} diff --git a/modules/web/server/src/main/scala/navigate/web/server/security/LdapConnectionOps.scala b/modules/web/server/src/main/scala/navigate/web/server/security/LdapConnectionOps.scala deleted file mode 100644 index f91876c0..00000000 --- a/modules/web/server/src/main/scala/navigate/web/server/security/LdapConnectionOps.scala +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) -// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause - -package navigate.web.server.security - -import com.unboundid.ldap.sdk.SearchRequest -import com.unboundid.ldap.sdk.SearchScope -import com.unboundid.ldap.sdk.* -import navigate.model.security.UserDetails.* - -import scala.jdk.CollectionConverters.* - -object LdapConnectionOps { - // Extension methods for ldap connection - extension (c: LDAPConnection) { - def authenticate(u: String, p: String): UID = { - val UidExtractor = s"([\\w\\.]*)(\\@.*)?".r - - val bindRequest = new SimpleBindRequest(u, p) - // Authenticate, it throws an exception if it fails - c.bind(bindRequest) - // Uid shouldn't have domain - u match { - case UidExtractor(uid, _) => uid - case uid => uid - } - } - - def displayName(uid: UID): DisplayName = { - val dn = for { - a <- attributes(uid, List("displayName")).get("displayName") - d <- a.headOption - } yield d - dn.getOrElse("-") - } - - def nameGroupsThumb(uid: UID): (DisplayName, Groups, Option[Thumbnail]) = { - val attrs = attributes(uid, List("displayName", "memberOf", "thumbnailPhoto")) - - val dn = for { - a <- attrs.get("displayName") - d <- a.headOption.filter(_ => false) - } yield d - - // Read the groups - val gr = attrs.getOrElse("memberOf", Nil) - val groups = gr.map { g => - val grDN = new DN(g) - for { - rdn <- grDN.getRDNs.toList - if rdn.hasAttribute("CN") && !rdn.hasAttributeValue("CN", "Users") - } yield rdn.getAttributeValues.toList - } - - // Read the thumbnail if possible - val thBytes = for { - ph <- attrs.get("thumbnailPhoto") - th <- ph.headOption - } yield th.getBytes - - (dn.getOrElse("-"), groups.flatten.flatten, thBytes) - } - - // Search for a user and find attributes. All attributes are String in LDAP - private def attributes(uid: UID, attributes: List[String]): Map[String, List[String]] = { - def readAttr(e: SearchResultEntry)(attr: String): Option[(String, List[String])] = - for { - a <- Option(e.getAttribute(attr)) - d <- Option(a.getValues.toList) - } yield attr -> d - - val baseDN = c.getRootDSE.getAttributeValue("namingContexts") - val filter = Filter.createANDFilter( - Filter.createEqualityFilter("sAMAccountName", uid), - Filter.createEqualityFilter("objectClass", "user") - ) - - // val attributes = List("displayName", "memberOf", "thumbnailPhoto") - val search = new SearchRequest(s"cn=users,$baseDN", SearchScope.SUB, filter, attributes: _*) - // Search to read user data, it may throw an exception - val searchResult = c.search(search) - - val r = for { - s <- searchResult.getSearchEntries.asScala.headOption - } yield attributes.map(readAttr(s)) - r.map(_.collect { case Some(i) => i }.toMap).getOrElse(Map.empty) - } - } -}