gothic (empty) → 0.1.0
raw patch · 9 files changed
+767/−0 lines, 9 filesdep +aesondep +basedep +binarysetup-changed
Dependencies added: aeson, base, binary, bytestring, connection, exceptions, hashable, http-client, http-client-tls, http-conduit, http-types, lens, lens-aeson, scientific, text, unix, unordered-containers, vector
Files
- Database/Vault/KVv2/Client.hs +233/−0
- Database/Vault/KVv2/Client/Internal.hs +102/−0
- Database/Vault/KVv2/Client/Lens.hs +94/−0
- Database/Vault/KVv2/Client/Requests.hs +121/−0
- Database/Vault/KVv2/Client/Types.hs +127/−0
- LICENSE +28/−0
- ReadMe.md +8/−0
- Setup.hs +2/−0
- gothic.cabal +52/−0
+ Database/Vault/KVv2/Client.hs view
@@ -0,0 +1,233 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++--------------------------------------------------------------------------------------------------+-- | See https://www.vaultproject.io/api/secret/kv/kv-v2.html for HashiCorp Vault KVv2 API details+--------------------------------------------------------------------------------------------------++module Database.Vault.KVv2.Client (++ VaultConnection,++ -- * Connect & configure Vault KVv2 Engine+ vaultConnect,+ kvEngineConfig,+ secretConfig,+ + -- * Basic operations++ putSecret,+ getSecret,+ + -- * Soft secret deletion+ deleteSecret,+ deleteSecretVersions,+ unDeleteSecretVersions,+ + -- * Permanent secret deletion+ destroySecret,+ destroySecretVersions,+ + -- * Get informations++ currentSecretVersion,+ readSecretMetadata,+ secretsList,++ -- * Utils+ toSecretData,+ fromSecretData,+ toSecretVersions,++ ) where++import qualified Data.Aeson as A+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as C+import Data.HashMap.Strict+import qualified Data.Maybe as M+import Data.Text hiding (concat)+import Network.Connection +import Network.HTTP.Client.TLS+import System.Environment (lookupEnv)+import System.Posix.Files (fileExist)++import Database.Vault.KVv2.Client.Types+import Database.Vault.KVv2.Client.Lens+import Database.Vault.KVv2.Client.Requests++-- | Get a 'VaultConnection', or an error message.+--+-- >λ: vaultConnect (Just "https://vault.local.lan:8200/") "/secret" Nothing False+--+vaultConnect+ :: Maybe String -- ^ Use 'Just' this string as Vault address or get it from variable environment VAULT_ADDR+ -> String -- ^ KV engine path+ -> Maybe VaultToken -- ^ Use 'Just' this 'VaultToken' or get it from $HOME/.vaut-token+ -> Bool -- ^ Disable certificate validation+ -> IO (Either String VaultConnection)+vaultConnect mva kvep mvt dcv = do+ nm <- newTlsManagerWith $+ mkManagerSettings+ TLSSettingsSimple+ { settingDisableCertificateValidation = dcv+ , settingDisableSession = False+ , settingUseServerName = True+ }+ Nothing+ va <- case mva of+ Just va -> return (Just va)+ Nothing -> lookupEnv "VAULT_ADDR"+ evt <- case mvt of+ Just t -> return (Right $ C.pack t)+ Nothing -> do+ hm <- lookupEnv "HOME"+ if M.isJust hm+ then do+ let fp = M.fromJust hm ++ "/.vault-token"+ if M.isJust va+ then do+ fe <- fileExist fp+ if fe + then Right <$> B.readFile fp+ else return (Left $ "No Vault token file found at " ++ fp)+ else return (Left "Variable environment VAULT_ADDR not set")+ else return (Left "Variable environment HOME not set")+ pure $+ (\vt ->+ VaultConnection+ { vaultAddr = M.fromJust va+ , vaultToken = vt+ , kvEnginePath = kvep+ , manager = nm+ }+ ) <$> evt++-- | Set default secret settings for the KVv2 engine.+kvEngineConfig+ :: VaultConnection+ -> Int -- ^ Max versions+ -> Bool -- ^ CAS required+ -> IO (Either String A.Value)+kvEngineConfig vc@VaultConnection{..} =+ configR ["POST ", show vc, "/config"] vc++-- | Override default secret settings for the given secret.+secretConfig+ :: VaultConnection+ -> SecretPath+ -> Int -- ^ Max versions+ -> Bool -- ^ CAS required+ -> IO (Either String A.Value)+secretConfig vc@VaultConnection{..} SecretPath{..} =+ configR ["POST ", show vc, "/metadata/", path] vc++-- | Get a secret from Vault. Give 'Just' the 'SecretVersion'+-- to retrieve or 'Nothing' to get the current one.+--+-- >λ>getSecret conn (SecretPath "MySecret") Nothing+-- >Right (SecretData (fromList [("my","password")]))+--+getSecret+ :: VaultConnection+ -> SecretPath+ -> Maybe SecretVersion+ -> IO (Either String SecretData)+getSecret vc sp msv =+ (>>= secret) <$> getSecretR vc sp msv++-- | Put 'SecretData' into Vault at the given location.+putSecret+ :: VaultConnection+ -> CheckAndSet -- ^ 'WriteAllowed', 'CreateOnly' or 'CurrentVersion'+ -> SecretPath+ -> SecretData -- ^ Data to put at 'SecretPath' location+ -> IO (Either String SecretVersion)+putSecret vc cas sp sd =+ (>>= version) <$> putSecretR vc cas sp sd++deleteSecret+ :: VaultConnection+ -> SecretPath+ -> IO (Maybe Error)+deleteSecret vc sp =+ maybeError <$> deleteSecretR vc sp++deleteSecretVersions+ :: VaultConnection+ -> SecretPath+ -> SecretVersions+ -> IO (Maybe Error)+deleteSecretVersions vc@VaultConnection{..} SecretPath{..} svs =+ maybeError <$> secretVersionsR ["POST ", show vc, "/delete/", path] vc svs++unDeleteSecretVersions+ :: VaultConnection+ -> SecretPath+ -> SecretVersions+ -> IO (Maybe Error)+unDeleteSecretVersions vc@VaultConnection{..} SecretPath{..} svs =+ maybeError <$> secretVersionsR ["POST ", show vc, "/undelete/", path] vc svs++-- | Permanently delete a secret, i.e. all its versions and metadata.+destroySecret+ :: VaultConnection+ -> SecretPath+ -> IO (Maybe Error)+destroySecret vc sp =+ maybeError <$> destroySecretR vc sp++destroySecretVersions+ :: VaultConnection+ -> SecretPath+ -> SecretVersions+ -> IO (Either String A.Value)+destroySecretVersions vc@VaultConnection{..} SecretPath{..} =+ secretVersionsR ["POST ", show vc, "/destroy/", path] vc++-- | Get list of secrets and folders at the given location.+secretsList+ :: VaultConnection+ -> SecretPath+ -> IO (Either String [VaultKey])+secretsList vc sp =+ (>>= list) <$> secretsListR vc sp++-- | Retrieve versions history of the given secret.+--+-- >λ: readSecretMetadata conn (SecretPath "MySecret") +-- >Right (SecretMetadata (fromList [(SecretVersion 1,Metadata {destroyed = True, deletion_time = "", created_time = "2019-05-30T13:22:58.416399224Z"}),(SecretVersion 2,Metadata {destroyed = True, deletion_time = "2019-06-29T15:28:46.145302138Z"})]))+--+readSecretMetadata+ :: VaultConnection+ -> SecretPath+ -> IO (Either String SecretMetadata)+readSecretMetadata vc sp =+ (>>= metadata) <$> readSecretMetadataR vc sp++-- | Get version number of the current given secret.+currentSecretVersion+ :: VaultConnection+ -> SecretPath+ -> IO (Either String SecretVersion)+currentSecretVersion vc sp =+ (>>= current) <$> readSecretMetadataR vc sp++-- Utils++toSecretData+ :: [(Text,Text)]+ -> SecretData+toSecretData = SecretData . fromList++fromSecretData+ :: SecretData+ -> [(Text,Text)]+fromSecretData (SecretData sd) = toList sd++toSecretVersions+ :: [Int]+ -> SecretVersions+toSecretVersions is =+ SecretVersions (SecretVersion <$> is)+
+ Database/Vault/KVv2/Client/Internal.hs view
@@ -0,0 +1,102 @@+{-# LANGUAGE OverloadedStrings #-}++module Database.Vault.KVv2.Client.Internal where++import Control.Lens+import Control.Monad.Catch+import qualified Data.ByteString as B+import qualified Data.Aeson as A+import Data.Aeson.Lens+import qualified Data.Maybe as M+import Data.Scientific+import Data.List as L+import Data.Text as T+import Network.HTTP.Client+import Network.HTTP.Types.Header+import qualified Data.Vector as V++runRequest+ :: Manager+ -> Request+ -> IO (Either String A.Value)+runRequest m r =+ esv <$> try (httpLbs r m)+ where+ esv t =+ case t of+ Right b ->+ pure (M.fromMaybe A.Null $ A.decode $ responseBody b)+ Left e -> Left $ show (e::SomeException)++fromVaultResponse+ :: T.Text+ -> (A.Value -> Either String a)+ -> A.Value+ -> Either String a+fromVaultResponse k f v =+ case v ^? key "data" . key k of+ Just o@(A.Object _) -> f o+ Just n@(A.Number _) -> f n+ Just a@(A.Array _) -> f a+ Just _ -> Left "Unexpected JSON type"+ Nothing -> Left (jsonErrors v)++vaultHeaders+ :: B.ByteString -- ^ Vault token+ -> [(HeaderName, B.ByteString)]+vaultHeaders vt =+ [ ("Content-Type", "application/json; charset=utf-8")+ , ("X-Vault-Token", vt)+ ]++toJSONName :: String -> String+toJSONName "secret_data" = "data"+toJSONName "secret_metadata" = "metadata"+toJSONName "response_data" = "data"+toJSONName s = s++jsonErrors :: A.Value -> String+jsonErrors v =+ case v ^? key "errors" of+ Just ja ->+ case ja of+ A.Array a ->+ if a == mempty+ then "Undetermined error"+ else+ L.intercalate+ ", "+ (toString <$> V.toList a) ++ "."+ _ -> "Unexpected JSON type"+ Nothing -> expectedJSONField "errors"++toString :: A.Value -> String+toString (A.String s) = T.unpack s+toString _ = fail "Expecting JSON type String only"++expectedJSONField :: String -> String+expectedJSONField f = "Expected JSON field not found: " ++ f++unexpectedJSONType :: Either String b+unexpectedJSONType = Left "Unexpected JSON type"++toInt :: Scientific -> Int+toInt = M.fromJust . toBoundedInteger++hasTrailingSlash :: String -> Bool+hasTrailingSlash s = s /= mempty && L.last s == '/'++removeTrailingSlash :: String -> String+removeTrailingSlash s =+ if hasTrailingSlash s+ then L.init s+ else s++hasLeadingSlash :: String -> Bool+hasLeadingSlash s = s /= mempty && L.head s == '/'++removeLeadingSlash :: String -> String+removeLeadingSlash s =+ if hasLeadingSlash s+ then L.tail s+ else s
+ Database/Vault/KVv2/Client/Lens.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE OverloadedStrings #-}+-- {-# LANGUAGE ExplicitForAll #-}+-- {-# LANGUAGE RankNTypes #-}++module Database.Vault.KVv2.Client.Lens (++ current,+ list,+ metadata,+ maybeError,+ secret,+ version++ ) where++import Control.Lens+import qualified Data.Aeson as A+import Data.Aeson.Lens+import qualified Data.Text as T+import qualified Data.Vector as V++import Database.Vault.KVv2.Client.Internal+import Database.Vault.KVv2.Client.Types++secret+ :: A.Value+ -> Either String SecretData+secret =+ fromVaultResponse "data" toSecretData+ where+ toSecretData o@(A.Object _) =+ case A.fromJSON o of+ A.Success sd -> Right sd+ A.Error e -> Left e+ toSecretData A.Null = Left "No current secret version"+ toSecretData _ = Left "Unexpected JSON type"++version+ :: A.Value+ -> Either String SecretVersion+version =+ fromVaultResponse "version" toSecretVersion+ where+ toSecretVersion (A.Number n) = Right (SecretVersion $ toInt n)+ toSecretVersion _ = undefined++current+ :: A.Value+ -> Either String SecretVersion+current =+ fromVaultResponse "current_version" toSecretVersion+ where+ toSecretVersion (A.Number n) = Right (SecretVersion $ toInt n)+ toSecretVersion _ = undefined++metadata+ :: A.Value+ -> Either String SecretMetadata+metadata =+ fromVaultResponse "versions" toSecretMetadata+ where+ toSecretMetadata o@(A.Object _) =+ case A.fromJSON o of+ A.Success vs -> Right vs+ A.Error e -> Left e+ toSecretMetadata _ = undefined++list+ :: A.Value+ -> Either String [VaultKey]+list =+ fromVaultResponse "keys" toListKeys+ where+ toListKeys (A.Array a) =+ Right (V.foldl lks mempty a)+ where+ lks ks (A.String t) =+ let s = T.unpack t in+ (if hasTrailingSlash s+ then VaultFolder s+ else VaultKey s) : ks+ lks p _ = p+ toListKeys _ = undefined+ +maybeError+ :: Either String A.Value+ -> Maybe Error+maybeError (Left s) = Just s+maybeError (Right v) =+ case v ^? key "data" . key "version" of+ Just A.Null -> Nothing+ Just _ -> Just "Unexpected JSON type"+ Nothing -> Just (jsonErrors v)+
+ Database/Vault/KVv2/Client/Requests.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Database.Vault.KVv2.Client.Requests (++ configR,+ getSecretR,+ putSecretR,+ deleteSecretR,+ destroySecretR,+ secretVersionsR,+ readSecretMetadataR,+ secretsListR++ ) where++import qualified Data.Aeson as A+import Network.HTTP.Client+import Network.HTTP.Simple ( setRequestBody+ , setRequestHeaders+ , setRequestBodyJSON+ )++import Database.Vault.KVv2.Client.Internal+import Database.Vault.KVv2.Client.Types++configR+ :: [String] -- ^ Endpoint+ ->VaultConnection+ -> Int -- ^ Max versions+ -> Bool -- ^ CAS required+ -> IO (Either String A.Value)+configR ss VaultConnection{..} mvs casr =+ parseRequest (concat ss)+ >>= runRequest manager+ . setRequestHeaders (vaultHeaders vaultToken)+ . setRequestBodyJSON+ SecretSettings+ { max_versions = mvs+ , cas_required = casr+ }++getSecretR+ :: VaultConnection+ -> SecretPath+ -> Maybe SecretVersion+ -> IO (Either String A.Value)+getSecretR vc@VaultConnection{..} SecretPath{..} msv =+ parseRequest+ (concat [show vc, "/data/", path, queryString msv])+ >>= runRequest manager . setRequestHeaders (vaultHeaders vaultToken)+ where+ queryString = maybe "" (\(SecretVersion v) -> "?version=" ++ show v) + +putSecretR+ :: VaultConnection+ -> CheckAndSet+ -> SecretPath+ -> SecretData+ -> IO (Either String A.Value)+putSecretR vc@VaultConnection{..} cas SecretPath{..} sd =+ parseRequest+ (concat ["POST ", show vc, "/data/", path])+ >>= runRequest manager+ . setRequestHeaders (vaultHeaders vaultToken)+ . setRequestBodyJSON+ PutSecretRequestBody+ { options = PutSecretOptions { cas = cas }+ , put_data = sd+ }++deleteSecretR+ :: VaultConnection+ -> SecretPath+ -> IO (Either String A.Value)+deleteSecretR vc@VaultConnection{..} SecretPath{..} =+ parseRequest+ (concat ["DELETE ", show vc, "/data/", path])+ >>= runRequest manager . setRequestHeaders (vaultHeaders vaultToken)++secretVersionsR+ :: [String] -- ^ Endpoint+ -> VaultConnection+ -> SecretVersions+ -> IO (Either String A.Value)+secretVersionsR ss VaultConnection{..} vs =+ parseRequest (concat ss) >>=+ runRequest manager+ . setRequestHeaders (vaultHeaders vaultToken)+ . setRequestBody (RequestBodyLBS $ A.encode vs)++destroySecretR+ :: VaultConnection+ -> SecretPath+ -> IO (Either String A.Value)+destroySecretR vc@VaultConnection{..} SecretPath{..} =+ parseRequest+ (concat ["DELETE ", show vc, "/metadata/", path])+ >>= runRequest manager . setRequestHeaders (vaultHeaders vaultToken)++secretsListR+ :: VaultConnection+ -> SecretPath+ -> IO (Either String A.Value)+secretsListR vc@VaultConnection{..} SecretPath{..} =+ if hasTrailingSlash path+ then+ parseRequest+ (concat ["LIST ", show vc, "/metadata/", path])+ >>= runRequest manager . setRequestHeaders (vaultHeaders vaultToken)+ else pure (Left "SecretPath must be a folder/")++readSecretMetadataR+ :: VaultConnection+ -> SecretPath+ -> IO (Either String A.Value)+readSecretMetadataR vc@VaultConnection{..} SecretPath{..} =+ parseRequest+ (concat ["GET ", show vc, "/metadata/", path])+ >>= runRequest manager . setRequestHeaders (vaultHeaders vaultToken)+
+ Database/Vault/KVv2/Client/Types.hs view
@@ -0,0 +1,127 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}++module Database.Vault.KVv2.Client.Types where++import Control.Monad (mzero)+import Data.Aeson+-- import Data.Binary -- TODO+import qualified Data.ByteString as B+import Data.HashMap.Strict+import Data.Hashable+import Data.Scientific+import qualified Data.Text as T+import Data.Text.Read (decimal)+import GHC.Generics+import Network.HTTP.Client (Manager)++import Database.Vault.KVv2.Client.Internal++type VaultToken = String++type Error = String++data VaultConnection =+ VaultConnection+ { vaultAddr :: !String+ , kvEnginePath :: !String+ , vaultToken :: !B.ByteString+ , manager :: !Manager+ }++instance Show VaultConnection where+ show (VaultConnection a p _ _) =+ removeTrailingSlash a ++ "/v1/"+ ++ removeTrailingSlash (removeLeadingSlash p)++newtype SecretVersions =+ SecretVersions [SecretVersion]+ deriving (Show, Eq)++instance ToJSON SecretVersions where+ toJSON (SecretVersions svs) =+ object+ [ "versions" .= ((\(SecretVersion i) -> i) <$> svs) ]++newtype SecretVersion+ = SecretVersion Int+ deriving (Show, Eq, Generic, Hashable)++newtype SecretMetadata =+ SecretMetadata (HashMap SecretVersion Metadata)+ deriving (Show, Eq)++instance FromJSON SecretMetadata where+ parseJSON (Object o) =+ pure (SecretMetadata . fromList $ trans <$> toList o)+ where+ trans p =+ case p of+ (t,j@(Object _)) -> do+ let Right (i,_) = decimal t+ let Success sv = fromJSON j+ (SecretVersion i,sv)+ _ -> undefined+ parseJSON _ = mzero++data Metadata =+ Metadata + { destroyed :: !Bool+ , deletion_time :: !T.Text+ , created_time :: !T.Text+ } deriving (Show, Eq, Generic, ToJSON, FromJSON)++newtype SecretData =+ SecretData+ (HashMap T.Text T.Text)+ deriving (Show, Generic, ToJSON, FromJSON)++-- instance Binary SecretData -- TODO++data SecretSettings =+ SecretSettings+ { max_versions :: Int+ , cas_required :: Bool+ } deriving (Show, Generic, ToJSON, FromJSON)+ +newtype SecretPath =+ SecretPath+ { path :: String }+ deriving (Show, Generic, ToJSON)++data CheckAndSet+ = WriteAllowed+ | CreateOnly+ | CurrentVersion !Int+ deriving (Show, Generic, ToJSON)++newtype PutSecretOptions =+ PutSecretOptions+ { cas :: CheckAndSet }+ deriving (Show)++instance ToJSON PutSecretOptions where+ toJSON PutSecretOptions { cas = WriteAllowed } = object []+ toJSON PutSecretOptions { cas = CreateOnly } = object [ "cas" .= Number 0.0 ]+ toJSON PutSecretOptions { cas = CurrentVersion v } =+ object [ "cas" .= Number (read (show v) :: Scientific) ]++data PutSecretRequestBody =+ PutSecretRequestBody+ { options :: PutSecretOptions+ , put_data :: SecretData+ }++instance ToJSON PutSecretRequestBody where+ toJSON (PutSecretRequestBody os sd) =+ object+ [ "options" .= os+ , "data" .= sd+ ]++data VaultKey+ = VaultKey !String+ | VaultFolder !String+ deriving (Show) +
+ LICENSE view
@@ -0,0 +1,28 @@+gothic - Copyright (c) 2019, Michel Boucey+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+ list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+ this list of conditions and the following disclaimer in the documentation+ and/or other materials provided with the distribution.++* Neither the name of gothic nor the names of its+ contributors may be used to endorse or promote products derived from+ this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+
+ ReadMe.md view
@@ -0,0 +1,8 @@++> "Historically, strongrooms were built in the basement of a bank where the ceilings were vaulted, hence the name."++<div style="text-align: right">Art. "Bank vault", Wikipedia.</div>++### Gothic, a Haskell client for Vault KV Engine v2 [](https://travis-ci.org/MichelBoucey/gothic)++This library implements the [HashiCorp Vault KVv2 Engine API](https://www.vaultproject.io/api/secret/kv/kv-v2.html).
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ gothic.cabal view
@@ -0,0 +1,52 @@+name: gothic+version: 0.1.0+synopsis: A Haskell Vault KVv2 secret engine client+description: A Haskell HashiCorp Vault KVv2 secret engine client library+homepage: https://github.com/MichelBoucey/gothic+license: BSD3+license-file: LICENSE+author: Michel Boucey+maintainer: michel.boucey@gmail.com+copyright: (c) 2019 - Michel Boucey+category: DevOps, Security, Database+build-type: Simple+cabal-version: >=1.10+extra-source-files: ReadMe.md++Tested-With: GHC ==8.4.3 || ==8.6.5++Source-Repository head+ Type: git+ Location: https://github.com/MichelBoucey/gothic++library+ exposed-modules: Database.Vault.KVv2.Client+ , Database.Vault.KVv2.Client.Internal+ , Database.Vault.KVv2.Client.Lens+ , Database.Vault.KVv2.Client.Requests+ , Database.Vault.KVv2.Client.Types++ other-extensions: OverloadedStrings++ build-depends: aeson >= 0.8.0.2 && < 1.5+ , base >= 4.8.1.0 && < 5+ , binary >= 0.8.5.1 && < 0.9+ , connection >= 0.2.8 && < 0.3+ , exceptions >= 0.10.0 && < 0.11+ , hashable >= 1.2.7.0 && < 1.3+ , http-conduit >= 2.3.2 && < 2.4+ , http-client >= 0.5.13.1 && < 0.6+ , http-client-tls >= 0.3.5.3 && < 0.4+ , http-types >= 0.12.2 && < 0.13+ , bytestring >= 0.10.8.2 && < 0.11+ , http-conduit >= 2.3.2 && < 2.4+ , text >= 1.2.3.0 && < 1.3+ , unordered-containers >= 0.2.9.0 && < 0.3+ , lens >= 4.16.1 && < 4.18+ , lens-aeson >= 1.0.2 && < 1.1+ , scientific >= 0.3.6.2 && < 0.4+ , unix >= 2.7.2.2 && < 2.8+ , vector >= 0.12.0.1 && < 0.13++ default-language: Haskell2010+ GHC-Options: -Wall