{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
module Aws.Iam.Core
( iamSignQuery
, iamResponseConsumer
, IamMetadata(..)
, IamConfiguration(..)
, IamError(..)
, parseDateTime
, AccessKeyStatus(..)
, User(..)
, parseUser
, MfaDevice(..)
, parseMfaDevice
) where
import Aws.Core
import qualified Blaze.ByteString.Builder as Blaze
import qualified Blaze.ByteString.Builder.Char8 as Blaze8
import Control.Exception (Exception)
import Control.Monad
import Control.Monad.Trans.Resource (MonadThrow, throwM)
import Data.ByteString (ByteString)
import Data.IORef
import Data.List (intersperse, sort)
import Data.Maybe
import Data.Monoid ()
import qualified Data.Semigroup as Sem
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Time
import Data.Typeable
import qualified Network.HTTP.Conduit as HTTP
import qualified Network.HTTP.Types as HTTP
#if !MIN_VERSION_time(1,5,0)
import System.Locale
#endif
import Text.XML.Cursor (($//))
import qualified Text.XML.Cursor as Cu
data IamError
= IamError {
iamStatusCode :: HTTP.Status
, iamErrorCode :: Text
, iamErrorMessage :: Text
}
deriving (Show, Typeable)
instance Exception IamError
data IamMetadata
= IamMetadata {
requestId :: Maybe Text
}
deriving (Show, Typeable)
instance Loggable IamMetadata where
toLogText (IamMetadata r) = "IAM: request ID=" Sem.<> fromMaybe "<none>" r
instance Sem.Semigroup IamMetadata where
IamMetadata r1 <> IamMetadata r2 = IamMetadata (r1 `mplus` r2)
instance Monoid IamMetadata where
mempty = IamMetadata Nothing
mappend = (Sem.<>)
data IamConfiguration qt
= IamConfiguration {
iamEndpoint :: ByteString
, iamPort :: Int
, iamProtocol :: Protocol
, iamHttpMethod :: Method
}
deriving (Show)
instance DefaultServiceConfiguration (IamConfiguration NormalQuery) where
defServiceConfig = iam PostQuery HTTPS iamEndpointDefault
debugServiceConfig = iam PostQuery HTTP iamEndpointDefault
instance DefaultServiceConfiguration (IamConfiguration UriOnlyQuery) where
defServiceConfig = iam Get HTTPS iamEndpointDefault
debugServiceConfig = iam Get HTTP iamEndpointDefault
iamEndpointDefault :: ByteString
iamEndpointDefault = "iam.amazonaws.com"
iam :: Method -> Protocol -> ByteString -> IamConfiguration qt
iam method protocol endpoint
= IamConfiguration {
iamEndpoint = endpoint
, iamProtocol = protocol
, iamPort = defaultPort protocol
, iamHttpMethod = method
}
iamSignQuery
:: [(ByteString, ByteString)]
-> IamConfiguration qt
-> SignatureData
-> SignedQuery
iamSignQuery q IamConfiguration{..} SignatureData{..}
= SignedQuery {
sqMethod = iamHttpMethod
, sqProtocol = iamProtocol
, sqHost = iamEndpoint
, sqPort = iamPort
, sqPath = "/"
, sqQuery = signedQuery
, sqDate = Just signatureTime
, sqAuthorization = Nothing
, sqContentType = Nothing
, sqContentMd5 = Nothing
, sqAmzHeaders = []
, sqOtherHeaders = []
, sqBody = Nothing
, sqStringToSign = stringToSign
}
where
sig = signature signatureCredentials HmacSHA256 stringToSign
signedQuery = ("Signature", Just sig):expandedQuery
accessKey = accessKeyID signatureCredentials
timestampHeader =
case signatureTimeInfo of
AbsoluteTimestamp time -> ("Timestamp", Just $ fmtAmzTime time)
AbsoluteExpires time -> ("Expires" , Just $ fmtAmzTime time)
newline = Blaze8.fromChar '\n'
stringToSign = Blaze.toByteString . mconcat . intersperse newline $
map Blaze.copyByteString
[httpMethod iamHttpMethod, iamEndpoint, "/"]
++ [HTTP.renderQueryBuilder False expandedQuery]
expandedQuery = HTTP.toQuery . sort $ (map (\(a,b) -> (a, Just b)) q ++) [
("AWSAccessKeyId" , Just accessKey)
, ("SignatureMethod" , Just $ amzHash HmacSHA256)
, ("SignatureVersion", Just "2")
, ("Version" , Just "2010-05-08")
, timestampHeader] ++
maybe [] (\tok -> [ ("SecurityToken", Just tok)]) (iamToken signatureCredentials)
iamResponseConsumer :: (Cu.Cursor -> Response IamMetadata a)
-> IORef IamMetadata
-> HTTPResponseConsumer a
iamResponseConsumer inner md resp = xmlCursorConsumer parse md resp
where
parse cursor = do
let rid = listToMaybe $ cursor $// elContent "RequestID"
tellMetadata $ IamMetadata rid
case cursor $// Cu.laxElement "Error" of
[] -> inner cursor
(err:_) -> fromError err
fromError cursor = do
errCode <- force "Missing Error Code" $ cursor $// elContent "Code"
errMsg <- force "Missing Error Message" $ cursor $// elContent "Message"
throwM $ IamError (HTTP.responseStatus resp) errCode errMsg
parseDateTime :: MonadThrow m => String -> m UTCTime
parseDateTime x
= case parseTimeM True defaultTimeLocale iso8601UtcDate x of
Nothing -> throwM $ XmlException $ "Invalid DateTime: " ++ x
Just dt -> return dt
data User
= User {
userArn :: Text
, userCreateDate :: UTCTime
, userPath :: Text
, userUserId :: Text
, userUserName :: Text
}
deriving (Eq, Ord, Show, Typeable)
parseUser :: MonadThrow m => Cu.Cursor -> m User
parseUser cursor = do
userArn <- attr "Arn"
userCreateDate <- attr "CreateDate" >>= parseDateTime . Text.unpack
userPath <- attr "Path"
userUserId <- attr "UserId"
userUserName <- attr "UserName"
return User{..}
where
attr name = force ("Missing " ++ Text.unpack name) $
cursor $// elContent name
data AccessKeyStatus = AccessKeyActive | AccessKeyInactive
deriving (Eq, Ord, Show, Typeable)
data MfaDevice = MfaDevice
{ mfaEnableDate :: UTCTime
, mfaSerialNumber :: Text
, mfaUserName :: Text
} deriving (Eq, Ord, Show, Typeable)
parseMfaDevice :: MonadThrow m => Cu.Cursor -> m MfaDevice
parseMfaDevice cursor = do
mfaEnableDate <- attr "EnableDate" >>= parseDateTime . Text.unpack
mfaSerialNumber <- attr "SerialNumber"
mfaUserName <- attr "UserName"
return MfaDevice{..}
where attr name = force ("Missing " ++ Text.unpack name) $
cursor $// elContent name