From 3d0980811d61a78f265ec06dd5bd4ef2cde1cbdf Mon Sep 17 00:00:00 2001 From: stuebinm Date: Sun, 22 Jan 2023 01:37:20 +0100 Subject: oauth2 via uffd this is unfortunately uffd-specific, since oauth2 is apparently sort of a vague standard. But since it doesn't actually do much it should probably be possible to make it fully configurable & generic if needed. --- app/Main.hs | 4 +- config.yaml | 10 +++- lib/Config.hs | 32 +++++++++-- lib/Server.hs | 13 ++--- lib/Server/ControlRoom.hs | 140 ++++++++++++++++++++++++++++++++++------------ lib/Yesod/Auth/Uffd.hs | 73 ++++++++++++++++++++++++ messages/de.msg | 2 + messages/en.msg | 2 + tracktrain.cabal | 8 ++- 9 files changed, 232 insertions(+), 52 deletions(-) create mode 100644 lib/Yesod/Auth/Uffd.hs diff --git a/app/Main.hs b/app/Main.hs index 204e4d7..f99a198 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -36,12 +36,12 @@ main = do >>= addSource (ConfAeson.fromFilePath "./config.json") >>= addSource (ConfEnv.fromConfig "tracktrain") - ServerConfig{..} <- fetch confconfig + settings@ServerConfig{..} <- fetch confconfig gtfs <- loadGtfs serverConfigGtfs serverConfigZoneinfoPath loggerMiddleware <- mkRequestLogger $ def { outputFormat = Detailed True } runStderrLoggingT $ withPostgresqlPool serverConfigDbString 10 $ \pool -> liftIO $ do - app <- application gtfs pool + app <- application gtfs pool settings putStrLn "starting server …" runSettings serverConfigWarp (loggerMiddleware app) diff --git a/config.yaml b/config.yaml index 49063e0..123031d 100644 --- a/config.yaml +++ b/config.yaml @@ -1,9 +1,17 @@ -dbString: "" + +dbstring: "dbname=tracktrain" gtfs: "./gtfs.zip" zoneinfoPath: "/etc/zoneinfo/" +# generic warp server options (see warp docs) warp: port: 9000 +# only oauth2 with uffd supported (for now) +login: + enable: true + clientname: tracktrain + clientsecret: secret + url: http://localhost:8080 diff --git a/lib/Config.hs b/lib/Config.hs index c76261e..65ac697 100644 --- a/lib/Config.hs +++ b/lib/Config.hs @@ -1,21 +1,31 @@ -{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE RecordWildCards #-} -- | module Config where -import Conferer (DefaultConfig (configDef), - FromConfig) +import Conferer (DefaultConfig (configDef)) +import Conferer.FromConfig import Conferer.FromConfig.Warp () import Data.ByteString (ByteString) import Data.Text (Text) import GHC.Generics (Generic) import Network.Wai.Handler.Warp (Settings) +import URI.ByteString + +data UffdConfig = UffdConfig + { uffdConfigUrl :: URIRef Absolute + , uffdConfigClientSecret :: Text + , uffdConfigClientName :: Text + , uffdConfigEnable :: Bool + } deriving (Generic, Show) data ServerConfig = ServerConfig { serverConfigWarp :: Settings , serverConfigDbString :: ByteString , serverConfigGtfs :: FilePath , serverConfigZoneinfoPath :: FilePath - } deriving Generic + , serverConfigLogin :: UffdConfig + } deriving (Generic) instance FromConfig ServerConfig @@ -25,4 +35,18 @@ instance DefaultConfig ServerConfig where , serverConfigDbString = "" , serverConfigGtfs = "./gtfs.zip" , serverConfigZoneinfoPath = "/etc/zoneinfo/" + , serverConfigLogin = configDef } + +instance DefaultConfig UffdConfig where + configDef = UffdConfig uri "secret" "uffdclient" False + where Right uri = parseURI strictURIParserOptions "http://www.example.org" + +instance FromConfig UffdConfig where + fromConfig key config = do + url <- fetchFromConfig (key /. "url") config + let Right uffdConfigUrl = parseURI strictURIParserOptions url + uffdConfigClientName <- fetchFromConfig (key /. "clientName") config + uffdConfigClientSecret <- fetchFromConfig (key /. "clientSecret") config + uffdConfigEnable <- fetchFromConfig (key /. "enable") config + pure UffdConfig {..} diff --git a/lib/Server.hs b/lib/Server.hs index 8cab47a..6b32826 100644 --- a/lib/Server.hs +++ b/lib/Server.hs @@ -57,7 +57,6 @@ import Server.Util (Service, ServiceM, runService, sendErrorMsg) import Yesod (toWaiAppPlain) -import Conferer (fetch, mkConfig) import Extrapolation (Extrapolator (..), LinearExtrapolator (..)) import System.IO.Unsafe @@ -68,14 +67,14 @@ import Data.ByteString.Lazy (toStrict) import Prometheus import Prometheus.Metric.GHC -application :: GTFS -> Pool SqlBackend -> IO Application -application gtfs dbpool = do +application :: GTFS -> Pool SqlBackend -> ServerConfig -> IO Application +application gtfs dbpool settings = do doMigration dbpool metrics <- Metrics <$> register (gauge (Info "ws_connections" "Number of WS Connections")) register ghcMetrics subscribers <- atomically $ newTVar mempty - pure $ serve (Proxy @CompleteAPI) $ hoistServer (Proxy @CompleteAPI) runService $ server gtfs metrics subscribers dbpool + pure $ serve (Proxy @CompleteAPI) $ hoistServer (Proxy @CompleteAPI) runService $ server gtfs metrics subscribers dbpool settings -- databaseMigration :: ConnectionString -> IO () doMigration pool = runSql pool $ @@ -84,14 +83,14 @@ doMigration pool = runSql pool $ -- returns an empty list runMigration migrateAll -server :: GTFS -> Metrics -> TVar (M.Map TripID ([TQueue (Maybe TrainPing)])) -> Pool SqlBackend -> Service CompleteAPI -server gtfs@GTFS{..} Metrics{..} subscribers dbpool = handleDebugAPI +server :: GTFS -> Metrics -> TVar (M.Map TripID ([TQueue (Maybe TrainPing)])) -> Pool SqlBackend -> ServerConfig -> Service CompleteAPI +server gtfs@GTFS{..} Metrics{..} subscribers dbpool settings = handleDebugAPI :<|> (handleStations :<|> handleTimetable :<|> handleTrip :<|> handleRegister :<|> handleTrainPing (throwError err401) :<|> handleWS :<|> handleSubscribe :<|> handleDebugState :<|> handleDebugTrain :<|> handleDebugRegister :<|> gtfsRealtimeServer gtfs dbpool) :<|> metrics - :<|> pure (unsafePerformIO (toWaiAppPlain (ControlRoom gtfs dbpool))) + :<|> pure (unsafePerformIO (toWaiAppPlain (ControlRoom gtfs dbpool settings))) where handleStations = pure stations handleTimetable station maybeDay = do -- TODO: resolve "overlay" trips (perhaps just additional CalendarDates?) diff --git a/lib/Server/ControlRoom.hs b/lib/Server/ControlRoom.hs index ee0f686..11e72d5 100644 --- a/lib/Server/ControlRoom.hs +++ b/lib/Server/ControlRoom.hs @@ -17,57 +17,66 @@ module Server.ControlRoom (ControlRoom(..)) where -import Control.Monad (forM_, join) -import Control.Monad.Extra (maybeM) -import Control.Monad.IO.Class (MonadIO (liftIO)) -import qualified Data.ByteString.Char8 as C8 -import Data.Functor ((<&>)) -import Data.List (lookup) -import Data.List.NonEmpty (nonEmpty) -import Data.Map (Map) -import qualified Data.Map as M -import Data.Pool (Pool) -import Data.Text (Text) -import qualified Data.Text as T -import Data.Time (UTCTime (..), getCurrentTime, - utctDay) -import Data.Time.Calendar (Day) -import Data.Time.Format.ISO8601 (iso8601Show) -import Data.UUID (UUID) -import qualified Data.UUID as UUID -import qualified Data.Vector as V -import Database.Persist (Entity (..), delete, entityVal, get, - insert, selectList, (==.)) -import Database.Persist.Sql (PersistFieldSql, SqlBackend, - runSqlPool) -import Fmt ((+|), (|+)) -import GHC.Float (int2Double) -import GHC.Generics (Generic) -import Server.Util (Service, secondsNow) -import Text.Blaze.Html (ToMarkup (..)) -import Text.Blaze.Internal (MarkupM (Empty)) -import Text.ProtocolBuffers (Default (defaultValue)) -import Text.Read (readMaybe) +import Control.Monad (forM_, join) +import Control.Monad.Extra (maybeM) +import Control.Monad.IO.Class (MonadIO (liftIO)) +import qualified Data.Aeson as A +import qualified Data.ByteString.Char8 as C8 +import qualified Data.ByteString.Lazy as LB +import Data.Functor ((<&>)) +import Data.List (lookup) +import Data.List.NonEmpty (nonEmpty) +import Data.Map (Map) +import qualified Data.Map as M +import Data.Pool (Pool) +import Data.Text (Text) +import qualified Data.Text as T +import Data.Time (UTCTime (..), getCurrentTime, + utctDay) +import Data.Time.Calendar (Day) +import Data.Time.Format.ISO8601 (iso8601Show) +import Data.UUID (UUID) +import qualified Data.UUID as UUID +import qualified Data.Vector as V +import Database.Persist (Entity (..), delete, entityVal, get, + insert, selectList, (==.)) +import Database.Persist.Sql (PersistFieldSql, SqlBackend, + runSqlPool) +import Fmt ((+|), (|+)) +import GHC.Float (int2Double) +import GHC.Generics (Generic) +import Server.Util (Service, secondsNow) +import Text.Blaze.Html (ToMarkup (..)) +import Text.Blaze.Internal (MarkupM (Empty)) +import Text.ProtocolBuffers (Default (defaultValue)) +import Text.Read (readMaybe) import Text.Shakespeare.Text import Yesod +import Yesod.Auth +import Yesod.Auth.OAuth2.Prelude import Yesod.Form -import Extrapolation (Extrapolator (..), - LinearExtrapolator (..)) +import Config (ServerConfig (..), UffdConfig (..)) +import Extrapolation (Extrapolator (..), + LinearExtrapolator (..)) import GTFS -import Numeric (showFFloat) +import Numeric (showFFloat) import Persist +import Yesod.Auth.OpenId (IdentifierType (..), authOpenId) +import Yesod.Auth.Uffd (UffdUser (..), uffdClient) data ControlRoom = ControlRoom - { getGtfs :: GTFS - , getPool :: Pool SqlBackend + { getGtfs :: GTFS + , getPool :: Pool SqlBackend + , getSettings :: ServerConfig } mkMessage "ControlRoom" "messages" "en" mkYesod "ControlRoom" [parseRoutes| / RootR GET +/auth AuthR Auth getAuth /trains TrainsR GET /train/id/#TripID/#Day TrainViewR GET /train/map/#TripID/#Day TrainMapViewR GET @@ -85,6 +94,18 @@ emptyMarkup (Empty _) = True emptyMarkup _ = False instance Yesod ControlRoom where + authRoute _ = Just $ AuthR LoginR + isAuthorized OnboardUnitMenuR _ = pure Authorized + isAuthorized (OnboardUnitR _ _) _ = pure Authorized + isAuthorized (AuthR _) _ = pure Authorized + isAuthorized _ _ = do + UffdConfig{..} <- getYesod <&> getSettings <&> serverConfigLogin + if uffdConfigEnable then maybeAuthId >>= \case + Just _ -> pure Authorized + Nothing -> pure AuthenticationRequired + else pure Authorized + + defaultLayout w = do PageContent{..} <- widgetToPageContent w msgs <- getMessages @@ -152,6 +173,48 @@ instance YesodPersist ControlRoom where pool <- getYesod <&> getPool runSqlPool action pool + +-- this instance is only slightly cursed (it keeps login information +-- as json in a session cookie and hopes nothing will ever go wrong) +instance YesodAuth ControlRoom where + type AuthId ControlRoom = UffdUser + + authPlugins cr = case config of + UffdConfig {..} -> if uffdConfigEnable + then [ uffdClient uffdConfigUrl uffdConfigClientName uffdConfigClientSecret ] + else [] + where config = serverConfigLogin (getSettings cr) + + maybeAuthId = do + e <- lookupSession "json" + pure $ case e of + Nothing -> Nothing + Just extra -> A.decode (LB.fromStrict $ C8.pack $ T.unpack extra) + + authenticate creds = do + forM_ (credsExtra creds) $ \(key, val) -> + setSession key val + -- extra <- lookupSession "extra" + -- pure (Authenticated ( undefined)) + e <- lookupSession "json" + case e of + Nothing -> error "no session information" + Just extra -> case A.decode (LB.fromStrict $ C8.pack $ T.unpack extra) of + Nothing -> error "malformed session information" + Just user -> pure $ Authenticated user + + loginDest _ = RootR + logoutDest _ = RootR + -- hardcode redirecting to uffd directly; showing the normal login + -- screen is kinda pointless when there's only one option + loginHandler = do + redirect ("/auth/page/uffd/forward" :: Text) + onLogout = do + clearSession + + + + getRootR :: Handler Html getRootR = redirect TrainsR @@ -159,6 +222,7 @@ getTrainsR :: Handler Html getTrainsR = do req <- getRequest let maybeDay = lookup "day" (reqGetParams req) >>= (readMaybe . T.unpack) + mdisplayname <- maybeAuthId <&> fmap uffdDisplayName day <- liftIO $ maybeM (getCurrentTime <&> utctDay) pure (pure maybeDay) gtfs <- getYesod <&> getGtfs @@ -166,6 +230,8 @@ getTrainsR = do defaultLayout $ do [whamlet|
_{MsgLoggedInAs name} - _{MsgLogout}
$forall trip@Trip{..} <- trips