module Network.VaultTool
( VaultAddress(..)
, VaultUnsealKey(..)
, VaultAuthToken(..)
, VaultException(..)
, VaultHealth(..)
, vaultHealth
, VaultConnection
, connectToVault
, vaultInit
, VaultSealStatus(..)
, vaultSealStatus
, vaultSeal
, VaultUnseal(..)
, vaultUnseal
, VaultMount(..)
, VaultMountRead
, VaultMountWrite
, VaultMountConfig(..)
, VaultMountConfigRead
, VaultMountConfigWrite
, vaultMounts
, vaultMountTune
, vaultMountSetTune
, vaultNewMount
, vaultUnmount
, VaultSecretPath(..)
, VaultSecretMetadata(..)
, vaultWrite
, vaultRead
, vaultDelete
, vaultList
, isFolder
, vaultListRecursive
) where
import Control.Exception (throwIO)
import Control.Monad (liftM)
import Data.Aeson
import Data.Aeson.Types (parseEither)
import Data.List (sortOn)
import Data.Text (Text)
import Network.HTTP.Client (Manager, newManager)
import Network.HTTP.Client.TLS (tlsManagerSettings)
import qualified Data.HashMap.Strict as H
import qualified Data.Text as T
import Network.VaultTool.Internal
import Network.VaultTool.Types
data VaultConnection = VaultConnection
{ _VaultConnection_AuthToken :: VaultAuthToken
, _VaultConnection_VaultAddress :: VaultAddress
, _VaultConnection_Manager :: Manager
}
data VaultHealth = VaultHealth
{ _VaultHealth_Version :: Text
, _VaultHealth_ServerTimeUtc :: Int
, _VaultHealth_Initialized :: Bool
, _VaultHealth_Sealed :: Bool
, _VaultHealth_Standby :: Bool
}
deriving (Show, Eq, Ord)
instance FromJSON VaultHealth where
parseJSON (Object v) =
VaultHealth <$>
v .: "version" <*>
v .: "server_time_utc" <*>
v .: "initialized" <*>
v .: "sealed" <*>
v .: "standby"
parseJSON _ = fail "Not an Object"
vaultUrl :: VaultAddress -> String -> String
vaultUrl (VaultAddress addr) path = T.unpack addr ++ "/v1" ++ path
vaultHealth :: VaultAddress -> IO VaultHealth
vaultHealth vaultAddress = do
manager <- newManager tlsManagerSettings
vaultRequestJSON manager "GET" (vaultUrl vaultAddress "/sys/health") [] (Nothing :: Maybe ()) expectedStatusCodes
where
expectedStatusCodes = [200, 429, 501, 503]
connectToVault :: VaultAddress -> VaultAuthToken -> IO VaultConnection
connectToVault addr authToken = do
manager <- newManager tlsManagerSettings
pure VaultConnection
{ _VaultConnection_AuthToken = authToken
, _VaultConnection_VaultAddress = addr
, _VaultConnection_Manager = manager
}
data VaultInitResponse = VaultInitResponse
{ _VaultInitResponse_Keys :: [Text]
, _VaultInitResponse_RootToken :: VaultAuthToken
}
deriving (Show, Eq, Ord)
instance FromJSON VaultInitResponse where
parseJSON (Object v) =
VaultInitResponse <$>
v .: "keys" <*>
v .: "root_token"
parseJSON _ = fail "Not an Object"
vaultInit
:: VaultAddress
-> Int
-> Int
-> IO ([VaultUnsealKey], VaultAuthToken)
vaultInit addr secretShares secretThreshold = do
let reqBody = object
[ "secret_shares" .= secretShares
, "secret_threshold" .= secretThreshold
]
manager <- newManager tlsManagerSettings
rsp <- vaultRequestJSON manager "PUT" (vaultUrl addr "/sys/init") [] (Just reqBody) [200]
let VaultInitResponse{_VaultInitResponse_Keys, _VaultInitResponse_RootToken} = rsp
pure (map VaultUnsealKey _VaultInitResponse_Keys, _VaultInitResponse_RootToken)
data VaultSealStatus = VaultSealStatus
{ _VaultSealStatus_Sealed :: Bool
, _VaultSealStatus_T :: Int
, _VaultSealStatus_N :: Int
, _VaultSealStatus_Progress :: Int
}
deriving (Show, Eq, Ord)
instance FromJSON VaultSealStatus where
parseJSON (Object v) =
VaultSealStatus <$>
v .: "sealed" <*>
v .: "t" <*>
v .: "n" <*>
v .: "progress"
parseJSON _ = fail "Not an Object"
vaultSealStatus :: VaultAddress -> IO VaultSealStatus
vaultSealStatus addr = do
manager <- newManager tlsManagerSettings
vaultRequestJSON manager "GET" (vaultUrl addr "/sys/seal-status") [] (Nothing :: Maybe ()) [200]
vaultSeal :: VaultConnection -> IO ()
vaultSeal VaultConnection{_VaultConnection_VaultAddress, _VaultConnection_Manager, _VaultConnection_AuthToken} = do
_ <- vaultRequest _VaultConnection_Manager "PUT" (vaultUrl _VaultConnection_VaultAddress "/sys/seal") headers (Nothing :: Maybe ()) [204]
pure ()
where
headers = [("X-Vault-Token", unVaultAuthToken _VaultConnection_AuthToken)]
data VaultUnseal
= VaultUnseal_Key VaultUnsealKey
| VaultUnseal_Reset
deriving (Show, Eq, Ord)
vaultUnseal :: VaultAddress -> VaultUnseal -> IO VaultSealStatus
vaultUnseal addr unseal = do
let reqBody = case unseal of
VaultUnseal_Key (VaultUnsealKey key) -> object
[ "key" .= key
]
VaultUnseal_Reset -> object
[ "reset" .= True
]
manager <- newManager tlsManagerSettings
vaultRequestJSON manager "PUT" (vaultUrl addr "/sys/unseal") [] (Just reqBody) [200]
type VaultMountRead = VaultMount Text VaultMountConfigRead
type VaultMountWrite = VaultMount (Maybe Text) (Maybe VaultMountConfigWrite)
type VaultMountConfigRead = VaultMountConfig Int
type VaultMountConfigWrite = VaultMountConfig (Maybe Int)
data VaultMount a b = VaultMount
{ _VaultMount_Type :: Text
, _VaultMount_Description :: a
, _VaultMount_Config :: b
}
deriving (Show, Eq, Ord)
instance FromJSON VaultMountRead where
parseJSON (Object v) =
VaultMount <$>
v .: "type" <*>
v .: "description" <*>
v .: "config"
parseJSON _ = fail "Not an Object"
instance ToJSON VaultMountWrite where
toJSON v = object
[ "type" .= _VaultMount_Type v
, "description" .= _VaultMount_Description v
, "config" .= _VaultMount_Config v
]
data VaultMountConfig a = VaultMountConfig
{ _VaultMountConfig_DefaultLeaseTtl :: a
, _VaultMountConfig_MaxLeaseTtl :: a
}
deriving (Show, Eq, Ord)
instance FromJSON VaultMountConfigRead where
parseJSON (Object v) =
VaultMountConfig <$>
v .: "default_lease_ttl" <*>
v .: "max_lease_ttl"
parseJSON _ = fail "Not an Object"
instance ToJSON VaultMountConfigWrite where
toJSON v = object
[ "default_lease_ttl" .= fmap formatSeconds (_VaultMountConfig_DefaultLeaseTtl v)
, "max_lease_ttl" .= fmap formatSeconds (_VaultMountConfig_MaxLeaseTtl v)
]
formatSeconds :: Int -> String
formatSeconds n = show n ++ "s"
vaultMounts :: VaultConnection -> IO [(Text, VaultMountRead)]
vaultMounts VaultConnection{_VaultConnection_VaultAddress, _VaultConnection_Manager, _VaultConnection_AuthToken} = do
let reqPath = vaultUrl _VaultConnection_VaultAddress "/sys/mounts"
rspObj <- vaultRequestJSON _VaultConnection_Manager "GET" reqPath headers (Nothing :: Maybe ()) [200]
let root = case H.lookup "data" rspObj of
Nothing -> Object rspObj
Just v -> v
case parseEither parseJSON root of
Left err -> throwIO $ VaultException_ParseBodyError "GET" reqPath (encode rspObj) err
Right obj -> pure $ sortOn fst (H.toList obj)
where
headers = [("X-Vault-Token", unVaultAuthToken _VaultConnection_AuthToken)]
vaultMountTune :: VaultConnection -> Text -> IO VaultMountConfigRead
vaultMountTune VaultConnection{_VaultConnection_VaultAddress, _VaultConnection_Manager, _VaultConnection_AuthToken} mountPoint = do
vaultRequestJSON _VaultConnection_Manager "GET" (vaultUrl _VaultConnection_VaultAddress "/sys/mounts/" ++ T.unpack mountPoint ++ "/tune") headers (Nothing :: Maybe ()) [200]
where
headers = [("X-Vault-Token", unVaultAuthToken _VaultConnection_AuthToken)]
vaultMountSetTune :: VaultConnection -> Text -> VaultMountConfigWrite -> IO ()
vaultMountSetTune VaultConnection{_VaultConnection_VaultAddress, _VaultConnection_Manager, _VaultConnection_AuthToken} mountPoint mountConfig = do
let reqBody = mountConfig
_ <- vaultRequest _VaultConnection_Manager "POST" (vaultUrl _VaultConnection_VaultAddress "/sys/mounts/" ++ T.unpack mountPoint ++ "/tune") headers (Just reqBody) [204]
pure ()
where
headers = [("X-Vault-Token", unVaultAuthToken _VaultConnection_AuthToken)]
vaultNewMount :: VaultConnection -> Text -> VaultMountWrite -> IO ()
vaultNewMount VaultConnection{_VaultConnection_VaultAddress, _VaultConnection_Manager, _VaultConnection_AuthToken} mountPoint vaultMount = do
let reqBody = vaultMount
_ <- vaultRequest _VaultConnection_Manager "POST" (vaultUrl _VaultConnection_VaultAddress "/sys/mounts/" ++ T.unpack mountPoint) headers (Just reqBody) [204]
pure ()
where
headers = [("X-Vault-Token", unVaultAuthToken _VaultConnection_AuthToken)]
vaultUnmount :: VaultConnection -> Text -> IO ()
vaultUnmount VaultConnection{_VaultConnection_VaultAddress, _VaultConnection_Manager, _VaultConnection_AuthToken} mountPoint = do
_ <- vaultRequest _VaultConnection_Manager "DELETE" (vaultUrl _VaultConnection_VaultAddress "/sys/mounts/" ++ T.unpack mountPoint) headers (Nothing :: Maybe ()) [204]
pure ()
where
headers = [("X-Vault-Token", unVaultAuthToken _VaultConnection_AuthToken)]
data VaultSecretMetadata = VaultSecretMetadata
{ _VaultSecretMetadata_leaseDuration :: Int
, _VaultSecretMetadata_leaseId :: Text
, _VauleSecretMetadata_renewable :: Bool
}
deriving (Show, Eq )
instance FromJSON VaultSecretMetadata where
parseJSON (Object v) =
VaultSecretMetadata <$>
v .: "lease_duration" <*>
v .: "lease_id" <*>
v .: "renewable"
parseJSON _ = fail "Not an Object"
vaultWrite :: ToJSON a => VaultConnection -> VaultSecretPath -> a -> IO ()
vaultWrite VaultConnection{_VaultConnection_VaultAddress, _VaultConnection_Manager, _VaultConnection_AuthToken} (VaultSecretPath location) value = do
let reqBody = value
_ <- vaultRequest _VaultConnection_Manager "POST" (vaultUrl _VaultConnection_VaultAddress "/" ++ T.unpack location) headers (Just reqBody) [204]
pure ()
where
headers = [("X-Vault-Token", unVaultAuthToken _VaultConnection_AuthToken)]
vaultRead
:: FromJSON a
=> VaultConnection
-> VaultSecretPath
-> IO (VaultSecretMetadata, Either (Value, String) a)
vaultRead VaultConnection{_VaultConnection_VaultAddress, _VaultConnection_Manager, _VaultConnection_AuthToken} (VaultSecretPath location) = do
let path = vaultUrl _VaultConnection_VaultAddress "/" ++ T.unpack location
rspObj <- vaultRequestJSON _VaultConnection_Manager "GET" path headers (Nothing :: Maybe ()) [200]
case parseEither parseJSON (Object rspObj) of
Left err -> throwIO $ VaultException_ParseBodyError "GET" path (encode rspObj) err
Right metadata -> case parseEither (.: "data") rspObj of
Left err -> throwIO $ VaultException_ParseBodyError "GET" path (encode rspObj) err
Right dataObj -> case parseEither parseJSON (Object dataObj) of
Left err -> pure (metadata, Left (Object dataObj, err))
Right data_ -> pure (metadata, Right data_)
where
headers = [("X-Vault-Token", unVaultAuthToken _VaultConnection_AuthToken)]
vaultDelete :: VaultConnection -> VaultSecretPath -> IO ()
vaultDelete VaultConnection{_VaultConnection_VaultAddress, _VaultConnection_Manager, _VaultConnection_AuthToken} (VaultSecretPath location) = do
_ <- vaultRequest _VaultConnection_Manager "DELETE" (vaultUrl _VaultConnection_VaultAddress "/" ++ T.unpack location) headers (Nothing :: Maybe ()) [204]
pure ()
where
headers = [("X-Vault-Token", unVaultAuthToken _VaultConnection_AuthToken)]
data VaultListResult = VaultListResult [Text]
instance FromJSON VaultListResult where
parseJSON (Object v) = do
data_ <- v .: "data"
keys <- data_ .: "keys"
pure (VaultListResult keys)
parseJSON _ = fail "Not an Object"
vaultList :: VaultConnection -> VaultSecretPath -> IO [VaultSecretPath]
vaultList VaultConnection{_VaultConnection_VaultAddress, _VaultConnection_Manager, _VaultConnection_AuthToken} (VaultSecretPath location) = do
VaultListResult keys <- vaultRequestJSON _VaultConnection_Manager "LIST" (vaultUrl _VaultConnection_VaultAddress "/" ++ T.unpack location) headers (Nothing :: Maybe ()) [200]
pure $ map (VaultSecretPath . (withTrailingSlash `T.append`)) keys
where
headers = [("X-Vault-Token", unVaultAuthToken _VaultConnection_AuthToken)]
withTrailingSlash
| T.null location = "/"
| T.last location == '/' = location
| otherwise = location `T.snoc` '/'
isFolder :: VaultSecretPath -> Bool
isFolder (VaultSecretPath path)
| T.null path = False
| otherwise = T.last path == '/'
vaultListRecursive :: VaultConnection -> VaultSecretPath -> IO [VaultSecretPath]
vaultListRecursive conn location = do
paths <- vaultList conn location
(flip concatMapM) paths $ \path -> do
if isFolder path
then vaultListRecursive conn path
else pure [path]
concatMapM :: (Monad m) => (a -> m [b]) -> [a] -> m [b]
concatMapM f xs = liftM concat (mapM f xs)