{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.AWS.Auth
(
getAuth
, Credentials (..)
, Auth
, envAccessKey
, envSecretKey
, envSessionToken
, credAccessKey
, credSecretKey
, credSessionToken
, credProfile
, credFile
, fromKeys
, fromSession
, fromEnv
, fromEnvKeys
, fromFile
, fromFilePath
, fromProfile
, fromProfileName
, AccessKey (..)
, SecretKey (..)
, SessionToken (..)
, AsAuthError (..)
, AuthError (..)
) where
import Control.Applicative
import Control.Concurrent
import Control.Monad
import Control.Monad.Catch
import Control.Monad.IO.Class
import Control.Monad.Trans.Maybe (MaybeT (..))
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy.Char8 as LBS8
import Data.Char (isSpace)
import qualified Data.Ini as INI
import Data.IORef
import Data.Monoid
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Time (diffUTCTime, getCurrentTime)
import Network.AWS.Data.Log
import Network.AWS.EC2.Metadata
import Network.AWS.Lens (catching, catching_, exception,
throwingM, _IOException)
import Network.AWS.Lens (Prism', prism, (<&>))
import Network.AWS.Prelude
import Network.AWS.Types
import Network.HTTP.Conduit
import System.Directory (doesFileExist, getHomeDirectory)
import System.Environment
import System.Mem.Weak
envAccessKey :: Text
envAccessKey = "AWS_ACCESS_KEY_ID"
envSecretKey :: Text
envSecretKey = "AWS_SECRET_ACCESS_KEY"
envSessionToken :: Text
envSessionToken = "AWS_SESSION_TOKEN"
envProfile :: Text
envProfile = "AWS_PROFILE"
envRegion :: Text
envRegion = "AWS_REGION"
credAccessKey :: Text
credAccessKey = "aws_access_key_id"
credSecretKey :: Text
credSecretKey = "aws_secret_access_key"
credSessionToken :: Text
credSessionToken = "aws_session_token"
credProfile :: Text
credProfile = "default"
credFile :: (MonadCatch m, MonadIO m) => m FilePath
credFile = catching_ _IOException dir err
where
dir = (++ p) `liftM` liftIO getHomeDirectory
err = throwM $ MissingFileError ("$HOME" ++ p)
p = "/.aws/credentials"
fromKeys :: AccessKey -> SecretKey -> Auth
fromKeys a s = Auth (AuthEnv a s Nothing Nothing)
fromSession :: AccessKey -> SecretKey -> SessionToken -> Auth
fromSession a s t = Auth (AuthEnv a s (Just t) Nothing)
data Credentials
= FromKeys AccessKey SecretKey
| FromSession AccessKey SecretKey SessionToken
| FromEnv Text Text (Maybe Text) (Maybe Text)
| FromProfile Text
| FromFile Text FilePath
| Discover
deriving (Eq)
instance ToLog Credentials where
build = \case
FromKeys a _ ->
"FromKeys " <> build a <> " ****"
FromSession a _ _ ->
"FromSession " <> build a <> " **** ****"
FromEnv a s t r ->
"FromEnv " <> build a <> " " <> build s <> " " <> m t <> " " <> m r
FromProfile n ->
"FromProfile " <> build n
FromFile n f ->
"FromFile " <> build n <> " " <> build f
Discover ->
"Discover"
where
m (Just x) = "(Just " <> build x <> ")"
m Nothing = "Nothing"
instance Show Credentials where
show = BS8.unpack . toBS . build
data AuthError
= RetrievalError HttpException
| MissingEnvError Text
| InvalidEnvError Text
| MissingFileError FilePath
| InvalidFileError Text
| InvalidIAMError Text
deriving (Show, Typeable)
instance Exception AuthError
instance ToLog AuthError where
build = \case
RetrievalError e -> build e
MissingEnvError e -> "[MissingEnvError] { message = " <> build e <> "}"
InvalidEnvError e -> "[InvalidEnvError] { message = " <> build e <> "}"
MissingFileError f -> "[MissingFileError] { path = " <> build f <> "}"
InvalidFileError e -> "[InvalidFileError] { message = " <> build e <> "}"
InvalidIAMError e -> "[InvalidIAMError] { message = " <> build e <> "}"
class AsAuthError a where
_AuthError :: Prism' a AuthError
{-# MINIMAL _AuthError #-}
_RetrievalError :: Prism' a HttpException
_MissingEnvError :: Prism' a Text
_InvalidEnvError :: Prism' a Text
_MissingFileError :: Prism' a FilePath
_InvalidFileError :: Prism' a Text
_InvalidIAMError :: Prism' a Text
_RetrievalError = _AuthError . _RetrievalError
_MissingEnvError = _AuthError . _MissingEnvError
_InvalidEnvError = _AuthError . _InvalidEnvError
_MissingFileError = _AuthError . _MissingFileError
_InvalidFileError = _AuthError . _InvalidFileError
_InvalidIAMError = _AuthError . _InvalidIAMError
instance AsAuthError SomeException where
_AuthError = exception
instance AsAuthError AuthError where
_AuthError = id
_RetrievalError = prism RetrievalError $ \case
RetrievalError e -> Right e
x -> Left x
_MissingEnvError = prism MissingEnvError $ \case
MissingEnvError e -> Right e
x -> Left x
_InvalidEnvError = prism InvalidEnvError $ \case
InvalidEnvError e -> Right e
x -> Left x
_MissingFileError = prism MissingFileError $ \case
MissingFileError f -> Right f
x -> Left x
_InvalidFileError = prism InvalidFileError $ \case
InvalidFileError e -> Right e
x -> Left x
_InvalidIAMError = prism InvalidIAMError $ \case
InvalidIAMError e -> Right e
x -> Left x
getAuth :: (Applicative m, MonadIO m, MonadCatch m)
=> Manager
-> Credentials
-> m (Auth, Maybe Region)
getAuth m = \case
FromKeys a s -> return (fromKeys a s, Nothing)
FromSession a s t -> return (fromSession a s t, Nothing)
FromEnv a s t r -> fromEnvKeys a s t r
FromProfile n -> fromProfileName m n
FromFile n f -> fromFilePath n f
Discover ->
catching_ _MissingEnvError fromEnv $
catching _MissingFileError fromFile $ \f -> do
p <- isEC2 m
unless p $
throwingM _MissingFileError f
fromProfile m
fromEnv :: (Applicative m, MonadIO m, MonadThrow m) => m (Auth, Maybe Region)
fromEnv =
fromEnvKeys
envAccessKey
envSecretKey
(Just envSessionToken)
(Just envRegion)
fromEnvKeys :: (Applicative m, MonadIO m, MonadThrow m)
=> Text
-> Text
-> Maybe Text
-> Maybe Text
-> m (Auth, Maybe Region)
fromEnvKeys access secret session region =
(,) <$> fmap Auth lookupKeys <*> lookupRegion
where
lookupKeys = AuthEnv
<$> (req access <&> AccessKey . BS8.pack)
<*> (req secret <&> SecretKey . BS8.pack)
<*> (opt session <&> fmap (SessionToken . BS8.pack))
<*> return Nothing
lookupRegion :: (MonadIO m, MonadThrow m) => m (Maybe Region)
lookupRegion = runMaybeT $ do
k <- MaybeT (return region)
r <- MaybeT (opt region)
case fromText (Text.pack r) of
Right x -> return x
Left e -> throwM . InvalidEnvError $
"Unable to parse ENV variable: " <> k <> ", " <> Text.pack e
req k = do
m <- opt (Just k)
maybe (throwM . MissingEnvError $ "Unable to read ENV variable: " <> k)
return
m
opt Nothing = return Nothing
opt (Just k) = liftIO (lookupEnv (Text.unpack k))
fromFile :: (Applicative m, MonadIO m, MonadCatch m) => m (Auth, Maybe Region)
fromFile = do
p <- liftIO (lookupEnv (Text.unpack envProfile))
fromFilePath (maybe credProfile Text.pack p)
=<< credFile
fromFilePath :: (Applicative m, MonadIO m, MonadCatch m)
=> Text
-> FilePath
-> m (Auth, Maybe Region)
fromFilePath n f = do
p <- liftIO (doesFileExist f)
unless p $
throwM (MissingFileError f)
ini <- either (invalidErr Nothing) return =<< liftIO (INI.readIniFile f)
env <- AuthEnv
<$> (req credAccessKey ini <&> AccessKey)
<*> (req credSecretKey ini <&> SecretKey)
<*> (opt credSessionToken ini <&> fmap SessionToken)
<*> return Nothing
return (Auth env, Nothing)
where
req k i =
case INI.lookupValue n k i of
Left e -> invalidErr (Just k) e
Right x
| blank x -> invalidErr (Just k) "cannot be a blank string."
| otherwise -> return (Text.encodeUtf8 x)
opt k i = return $
case INI.lookupValue n k i of
Left _ -> Nothing
Right x -> Just (Text.encodeUtf8 x)
invalidErr Nothing e = throwM $ InvalidFileError (Text.pack e)
invalidErr (Just k) e = throwM $ InvalidFileError
(Text.pack f <> ", key " <> k <> " " <> Text.pack e)
blank x = Text.null x || Text.all isSpace x
fromProfile :: (MonadIO m, MonadCatch m) => Manager -> m (Auth, Maybe Region)
fromProfile m = do
ls <- try $ metadata m (IAM (SecurityCredentials Nothing))
case BS8.lines `liftM` ls of
Right (x:_) -> fromProfileName m (Text.decodeUtf8 x)
Left e -> throwM (RetrievalError e)
_ -> throwM $
InvalidIAMError "Unable to get default IAM Profile from EC2 metadata"
fromProfileName :: (MonadIO m, MonadCatch m)
=> Manager
-> Text
-> m (Auth, Maybe Region)
fromProfileName m name = do
auth <- getCredentials >>= start
reg <- getRegion
return (auth, Just reg)
where
getCredentials :: (MonadIO m, MonadCatch m) => m AuthEnv
getCredentials =
try (metadata m (IAM . SecurityCredentials $ Just name)) >>=
handleErr (eitherDecode' . LBS8.fromStrict) invalidIAMErr
getRegion :: (MonadIO m, MonadCatch m) => m Region
getRegion =
try (identity m) >>=
handleErr (fmap _region) invalidIdentityErr
handleErr _ _ (Left e) = throwM (RetrievalError e)
handleErr f g (Right x) = either (throwM . g) return (f x)
invalidIAMErr = InvalidIAMError
. mappend ("Error parsing IAM profile '" <> name <> "' ")
. Text.pack
invalidIdentityErr = InvalidIAMError
. mappend "Error parsing Instance Identity Document "
. Text.pack
start :: MonadIO m => AuthEnv -> m Auth
start !a = liftIO $
case _authExpiry a of
Nothing -> return (Auth a)
Just x -> do
r <- newIORef a
p <- myThreadId
s <- timer r p x
return (Ref s r)
timer :: IORef AuthEnv -> ThreadId -> UTCTime -> IO ThreadId
timer !r !p !x = forkIO $ do
s <- myThreadId
w <- mkWeakIORef r (killThread s)
loop w p x
loop :: Weak (IORef AuthEnv) -> ThreadId -> UTCTime -> IO ()
loop w !p !x = do
diff x <$> getCurrentTime >>= threadDelay
env <- try getCredentials
case env of
Left e -> throwTo p (RetrievalError e)
Right !a -> do
mr <- deRefWeak w
case mr of
Nothing -> return ()
Just r -> do
atomicWriteIORef r a
maybe (return ()) (loop w p) (_authExpiry a)
diff !x !y = (* 1000000) $ if n > 0 then n else 1
where
!n = truncate (diffUTCTime x y) - 60