packages feed

aws-configuration-tools-0.1.0.0: src/Configuration/Utils/Aws/Credentials.hs

-- Copyright (c) 2013-2015 PivotCloud, Inc.
--
-- Configuration.Utils.Aws.Credentials
--
-- Please feel free to contact us at licensing@pivotmail.com with any
-- contributions, additions, or other feedback; we would love to hear from
-- you.
--
-- Licensed under the Apache License, Version 2.0 (the "License"); you may
-- not use this file except in compliance with the License. You may obtain a
-- copy of the License at http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-- License for the specific language governing permissions and limitations
-- under the License.

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE UnicodeSyntax #-}

-- |
-- Module: Configuration.Utils.Aws.Credentials
-- Copyright: Copyright © 2013-2015 PivotCloud, Inc.
-- License: Apache-2.0
-- Maintainer: Jon Sterling <jsterling@alephcloud.com>
-- Stability: experimental
--
module Configuration.Utils.Aws.Credentials
( -- * Configuration Types
  CredentialConfigKey(..)
, credentialConfigKeyId
, credentialConfigKeySecret
, validateCredentialConfigKey
, CredentialConfigFile(..)
, credentialConfigFileName
, credentialConfigKeyName
, validateCredentialConfigFile
, CredentialConfig(..)
, credentialConfigKey
, credentialConfigFile
, credentialConfigEnvironment
, credentialConfigInstanceMetadata
, defaultCredentialConfig
, pCredentialConfig
, pCredentialConfig_
, validateCredentialConfig

  -- * Load Credentials
, credentialsFromConfig
, LoadCredentialsException(..)
) where

import Aws

import Configuration.Utils
import Configuration.Utils.Internal
import Configuration.Utils.Validation

import Control.Exception
import Control.Monad
import Control.Monad.Error.Class
import Control.Monad.IO.Class
import Control.Monad.Unicode

import Data.Maybe
import Data.Monoid.Unicode
import Data.Traversable
import Data.Typeable

import qualified Data.ByteString.Char8 as B8
import qualified Data.Text as T

import Prelude.Unicode

-- -------------------------------------------------------------------------- --
-- Credential Configuration

data CredentialConfigKey
  = CredentialConfigKey
  { _credentialConfigKeyId ∷ !B8.ByteString
  , _credentialConfigKeySecret ∷ !B8.ByteString
  } deriving (Show, Read, Eq, Ord, Typeable)

credentialConfigKeyId ∷ Lens' CredentialConfigKey B8.ByteString
credentialConfigKeyId = lens _credentialConfigKeyId $ \a b → a { _credentialConfigKeyId = b }

credentialConfigKeySecret ∷ Lens' CredentialConfigKey B8.ByteString
credentialConfigKeySecret = lens _credentialConfigKeySecret $ \a b → a { _credentialConfigKeySecret = b }

validateCredentialConfigKey ∷ ConfigValidation CredentialConfigKey λ
validateCredentialConfigKey CredentialConfigKey{..} = do
  validateNonEmpty "access_key_id" _credentialConfigKeyId
  validateNonEmpty "access_key_secret" _credentialConfigKeySecret

instance ToJSON CredentialConfigKey where
  toJSON CredentialConfigKey{..} = object
    [ "access_key_id" .= B8.unpack _credentialConfigKeyId
    , "access_key_secret" .= B8.unpack _credentialConfigKeySecret
    ]

instance FromJSON CredentialConfigKey where
  parseJSON =
    withObject "CredentialConfigKey" $ \o →
      CredentialConfigKey
        <$> (B8.pack <$> o .: "access_key_id")
        <*> (B8.pack <$> o .: "access_key_secret")

data CredentialConfigFile
  = CredentialConfigFile
  { _credentialConfigFileName ∷ !FilePath
  , _credentialConfigKeyName ∷ !T.Text
      -- ^ default value is @"default"@
  } deriving (Show, Read, Eq, Ord, Typeable)

credentialConfigFileName ∷ Lens' CredentialConfigFile FilePath
credentialConfigFileName = lens _credentialConfigFileName $ \a b → a { _credentialConfigFileName = b }

credentialConfigKeyName ∷ Lens' CredentialConfigFile T.Text
credentialConfigKeyName = lens _credentialConfigKeyName $ \a b → a { _credentialConfigKeyName = b }

validateCredentialConfigFile ∷ ConfigValidation CredentialConfigFile λ
validateCredentialConfigFile CredentialConfigFile{..} =
  validateFileReadable "file_name" _credentialConfigFileName

instance ToJSON CredentialConfigFile where
  toJSON CredentialConfigFile{..} = object
    [ "file_name" .= _credentialConfigFileName
    , "key_name" .= _credentialConfigKeyName
    ]

instance FromJSON CredentialConfigFile where
  parseJSON =
    withObject "CredentialConfigFile" $ \o →
      CredentialConfigFile
        <$> o .: "file_name"
        <*> o .:? "key_name" .!= "default"


-- | When parsed from configuration files or the command line the settings take
-- the following precedence:
--
-- Explicit credential configuration gets precedence over credential file
-- configuration which has precedence over loading the credentials from the
-- environment over loading the credentials from the instance meta data.
--
-- Any setting on the command line always has precedence over any setting in a
-- configuration file.
--
-- If the configured file does not exist an parse error is triggered. Set this
-- value of '_credentialConfigFile' to 'Nothing' if you don't want to suppress
-- this error.
--
data CredentialConfig
  = CredentialConfig
  { _credentialConfigKey ∷ !(Maybe CredentialConfigKey)
  , _credentialConfigFile ∷ !(Maybe CredentialConfigFile)
  , _credentialConfigEnvironment ∷ !Bool
  , _credentialConfigInstanceMetadata ∷ !Bool
  } deriving (Show, Eq, Ord)

credentialConfigKey ∷ Lens' CredentialConfig (Maybe CredentialConfigKey)
credentialConfigKey = lens _credentialConfigKey $ \a b → a { _credentialConfigKey = b}

credentialConfigFile ∷ Lens' CredentialConfig (Maybe CredentialConfigFile)
credentialConfigFile = lens _credentialConfigFile $ \a b → a { _credentialConfigFile = b}

credentialConfigEnvironment ∷ Lens' CredentialConfig Bool
credentialConfigEnvironment = lens _credentialConfigEnvironment $ \a b → a { _credentialConfigEnvironment = b}

credentialConfigInstanceMetadata ∷ Lens' CredentialConfig Bool
credentialConfigInstanceMetadata = lens _credentialConfigInstanceMetadata $ \a b → a { _credentialConfigInstanceMetadata = b}

defaultCredentialConfig ∷ CredentialConfig
defaultCredentialConfig = CredentialConfig
  { _credentialConfigKey = Nothing
  , _credentialConfigFile = Nothing
  , _credentialConfigEnvironment = False
  , _credentialConfigInstanceMetadata = False
  }

validateCredentialConfig ∷ ConfigValidation CredentialConfig λ
validateCredentialConfig CredentialConfig{..} = do
  void $ for _credentialConfigKey validateCredentialConfigKey
  void $ for _credentialConfigFile validateCredentialConfigFile
  unless (or enabledList) $
    throwError "at least one credentials configuration option must be configured"

  where
    enabledList =
      [ isJust _credentialConfigKey
      , isJust _credentialConfigFile
      , _credentialConfigEnvironment
      , _credentialConfigInstanceMetadata
      ]

instance ToJSON CredentialConfig where
  toJSON CredentialConfig{..} = object
    [ "key" .= _credentialConfigKey
    , "file" .= _credentialConfigFile
    , "environment" .= _credentialConfigEnvironment
    , "metadata" .= _credentialConfigInstanceMetadata
    ]

instance FromJSON (CredentialConfig → CredentialConfig) where
  parseJSON =
    withObject "CredentialConfig" $ \o → id
      <$< credentialConfigKey ..: "key" % o
      <*< credentialConfigFile ..: "file" % o
      <*< credentialConfigEnvironment ..: "environment" % o
      <*< credentialConfigInstanceMetadata ..: "metadata" % o

-- | A command-line argument parser for 'CredentialConfig'. The @prefix@
-- argument will be prepended directly onto the argument names.
--
pCredentialConfig
  ∷ String -- ^ prefix
  → MParser CredentialConfig
pCredentialConfig prefix = id
  <$< credentialConfigKey .:: fmap Just % pCredentialConfigKey
  <*< credentialConfigFile .:: fmap Just % pCredentialConfigFile
  <*< credentialConfigEnvironment .:: boolOption
      % long (prefix ⊕ "credentials-from-environment")
      ⊕ help "load AWS access credentials from environment variables (AWS_ACCESS_KEY_ID and AWS_ACCESS_KEY_SECRET)"
  <*< credentialConfigInstanceMetadata .:: boolOption
      % long (prefix ⊕ "credentials-from-metadata")
      ⊕ help "load AWS access credentials from the instance metadata"

  where
    pKeyId =
      B8.pack <$> strOption
        % long (prefix ⊕ "credentials-key-id")
        ⊕ metavar "AWS_ACCESS_KEY_ID"
        ⊕ help "Id of the AWS access key"

    pKeySecret =
      B8.pack <$> strOption
        % long (prefix ⊕ "credentials-key-secret")
        ⊕ metavar "AWS_ACCESS_KEY_SECRET"
        ⊕ help "Secret of the AWS access key"

    pCredentialConfigKey =
      CredentialConfigKey <$> pKeyId <*> pKeySecret

    pKeyName =
      T.pack <$> strOption
        % long (prefix ⊕ "credentials-key-name")
        ⊕ metavar "STRING"
        ⊕ help "the name of the access key in the access key file"
        ⊕ value (T.unpack credentialsDefaultKey)

    pFileName =
      fileOption
        % long (prefix ⊕ "credentials-key-file")
        ⊕ help "the name of the file with access keys for the AWS API"

    pCredentialConfigFile =
      CredentialConfigFile <$> pFileName <*> pKeyName

-- | This is 'pCredentialConfig' with an empty prefix.
--
pCredentialConfig_ ∷ MParser CredentialConfig
pCredentialConfig_ = pCredentialConfig ""

data LoadCredentialsException
  = LoadCredentialsFromFileFailed CredentialConfigFile
  | LoadCredentialsFromEnvironementFailed
  | LoadCredentialsFromInstanceMetadataFailed
  | NoCredentialLoadingMethodSpecified
  deriving (Eq, Show, Typeable)

instance Exception LoadCredentialsException

-- | Load Credentials according to the credential configuration
--
-- Credentials are loaded with a precedence according to the order of
-- the record fields of 'CredentialConfig':
--
-- 1. '_credentialConfigKey', loads the explicitely specified credentials,
--
-- 2. '_credentialConfigFile', loads credentials with the given key name
--    from the given file,
--
-- 3. '_credentialConfigEnvironment', loads credentials from the
--    environment varialbes @AWS_ACCESS_KEY_ID@ and @AWS_ACCESS_KEY_SECRET@), and
--
-- 3. '_credentialConfigInstanceMetadata', load the credentials from the
--    EC2 instance metadata.
--
-- It is an error if a particular method is specified in the configuration but
-- loading the credentials with that methods fails.
--
credentialsFromConfig
  ∷ (MonadError LoadCredentialsException m, MonadIO m)
  ⇒ CredentialConfig
  → m Credentials
credentialsFromConfig CredentialConfig{..}
  | Just CredentialConfigKey{..} ← _credentialConfigKey =
      liftIO $ makeCredentials _credentialConfigKeyId _credentialConfigKeySecret

  | Just ccf@(CredentialConfigFile file awsKeyName) ← _credentialConfigFile =
      liftIO (loadCredentialsFromFile file awsKeyName) ≫= \case
        Nothing → throwError $ LoadCredentialsFromFileFailed ccf
        Just c → return c

  | _credentialConfigEnvironment =
      liftIO loadCredentialsFromEnv ≫= \case
        Nothing → throwError LoadCredentialsFromEnvironementFailed
        Just c → return c

  | _credentialConfigInstanceMetadata =
      liftIO loadCredentialsFromInstanceMetadata ≫= \case
        Nothing → throwError LoadCredentialsFromInstanceMetadataFailed
        Just c → return c

  | otherwise = throwError NoCredentialLoadingMethodSpecified