{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
module Main where
import AwsMfaCredentials.MainLoop (Opts(..), mainLoopBody)
import qualified AwsMfaCredentials.Effects.AWS as E
import AwsMfaCredentials.Effects.PasswordPrompt (PasswordPrompt)
import AwsMfaCredentials.Interpreters.AWS
(AWSResponseFailure(..), runInAWSMonad)
import AwsMfaCredentials.Interpreters.CredentialsWriter
(CredentialsFileParseError(..), writeCredentials)
import AwsMfaCredentials.Interpreters.PasswordPrompt
(RunAskPassFailure(..), runWithAskPass)
import AwsMfaCredentials.Interpreters.Wait (runWait)
import Control.Monad.Freer (Eff, Member, runM, runNat, send)
import Control.Monad.Freer.Exception (Exc, runError)
import Control.Monad.Freer.Writer (Writer)
import Control.Monad.IO.Class (liftIO)
import Data.Semigroup ((<>))
import Data.Text (Text)
import Data.Time (UTCTime)
import Network.AWS (AWS, newEnv, runAWS, Credentials(..), runResourceT)
import Network.AWS.Auth (credFile)
import qualified Network.AWS.STS.Types as STS
import Options.Applicative
import Options.Applicative.Text (textOption)
import System.IO (hPutStrLn, stderr)
-- | Parser for command line options
optsParser :: Parser Opts
optsParser = Opts
<$> textOption
( long "mfa-serial-number"
<> metavar "SERIAL_NUMBER"
<> help "The ID of the MFA device to authenticate with"
)
<*> (optional $ option auto
( long "duration"
<> metavar "DURATION_SECONDS"
<> help "The lifetime of the credentials (uses AWS default if unset)"
)
)
<*> textOption
( long "profile"
<> metavar "PROFILE"
<> help "The name of the profile whose credentials we're managing"
<> value "default"
<> showDefault
)
<*> option auto
( long "refresh-lead-time"
<> metavar "LEAD_SECONDS"
<> help "The amount of time before credential expiry to refresh the credentials"
<> value 300
<> showDefault
)
-- | Full command line parser with usage string.
optsParserInfo :: ParserInfo Opts
optsParserInfo = info (optsParser <**> helper)
( fullDesc
<> progDesc "Keep your AWS credentials file up to date with MFA-carrying temporary credentials"
)
handleError :: forall r a . (Member IO r)
=> (a -> String)
-> Eff (Exc a ': r) Bool
-> Eff r Bool
handleError showE e = runError e >>= \case
Left err -> do
send . hPutStrLn stderr $ showE err
return True
Right b -> return b
mainLoop :: Opts -> AWS ()
mainLoop opts = do
(interpret $ mainLoopBody opts) >>= \case
False -> mainLoop opts
True -> return ()
where
interpret :: Eff '[ Writer UTCTime
, Writer (Text, STS.Credentials)
, E.AWS
, PasswordPrompt String Text
, Exc CredentialsFileParseError
, Exc AWSResponseFailure
, Exc RunAskPassFailure
, IO
, AWS
] () -> AWS Bool
interpret =
runM
. runNat @AWS liftIO
. handleError runAskPassFailure
. handleError awsResponseFailure
. handleError credentialsFileParseError
. (False <$)
. runWithAskPass
. runInAWSMonad
. writeCredentials
. runWait
runAskPassFailure RunAskPassTimeout =
"Timed out waiting for MFA token"
runAskPassFailure (RunAskPassFailure _) =
"User cancelled token input"
awsResponseFailure (AWSResponseFailure i) =
"Requesting temporary credentials from AWS failed with HTTP error code " ++ show i
credentialsFileParseError (CredentialsFileParseError msg) =
"Error parsing the AWS credentials file: " ++ msg
main :: IO ()
main = do
opts <- execParser optsParserInfo
env <- credFile >>= newEnv . FromFile (profile opts)
runResourceT . runAWS env $ mainLoop opts