packages feed

aws-cloudfront-signed-cookies-0.2.0.9: library/Network/AWS/CloudFront/SignedCookies/Policy.hs

{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, TypeApplications #-}

module Network.AWS.CloudFront.SignedCookies.Policy
  (
  -- * Defining a policy
    Policy (..)
  , simplePolicy

  -- * Components of a policy
  , Resource (..)
  , StartTime (..)
  , EndTime (..)
  , Lifespan (..)
  , IpAddress (..)

  -- * JSON representation
  , policyJSON
  , jsonTextPolicy
  , jsonValPolicy

  ) where

import Network.AWS.CloudFront.SignedCookies.Types

-- aeson
import qualified Data.Aeson as A (Value, eitherDecode', encode, object)
import qualified Data.Aeson.Types as A (Pair)
import Data.Aeson ((.=), toJSON)

-- base
import Control.Monad ((>=>))
import Data.Semigroup ((<>))

-- bytestring
import qualified Data.ByteString.Lazy as LBS

-- lens
import Control.Lens ((&), (^.), (^?))

-- lens-aeson
import Data.Aeson.Lens (AsNumber (..), AsPrimitive (..), key, nth, _Array, _Object)

-- text
import qualified Data.Text.Encoding as Text

-- time
import Data.Time.Clock.POSIX (getPOSIXTime)

-- vector
import qualified Data.Vector as Vec

{- |

Encode a 'Policy' as JSON, with no whitespace, as AWS requires.

Excerpt from [Setting Signed Cookies Using a Custom Policy](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html):

* "Remove all whitespace (including tabs and newline characters) from the policy statement."

-}

policyJSON :: Policy -> ByteString
policyJSON =
  LBS.toStrict . A.encode . policyValue

policyValue :: Policy -> A.Value
policyValue policy = A.object [ "Statement" .= statement ]
  where
    statement = toJSON @[A.Value]
      [ A.object
        [ "Resource" .= resourceValue policy
        , "Condition" .= conditionValue policy
        ]
      ]

resourceValue :: Policy -> A.Value
resourceValue (Policy (Resource x) _ _ _) = toJSON @Text x

conditionValue :: Policy -> A.Value
conditionValue (Policy _ start end ip) =
  A.object $ startCondition <> endCondition <> ipCondition
  where

    startCondition :: [A.Pair] =
      case start of
        StartImmediately -> []
        StartTime x -> ["DateGreaterThan" .= posixTimeValue x]

    endCondition :: [A.Pair] =
      case end of
        EndTime x -> ["DateLessThan" .= posixTimeValue x]

    ipCondition :: [A.Pair] =
      case ip of
        AnyIp -> []
        IpAddress x -> ["IpAddress" .= sourceIpValue x]

posixTimeValue :: POSIXTime -> A.Value
posixTimeValue x = A.object ["AWS:EpochTime" .= toJSON @Integer (round x) ]

sourceIpValue :: Text -> A.Value
sourceIpValue x = A.object ["AWS:SourceIp" .= toJSON @Text x]

jsonTextPolicy :: Text -> Either String Policy
jsonTextPolicy =
  (A.eitherDecode' . LBS.fromStrict . Text.encodeUtf8) >=>
  jsonValPolicy

jsonValPolicy :: A.Value -> Either String Policy
jsonValPolicy val =
  Policy
    <$> jsonResource  val
    <*> jsonStart     val
    <*> jsonEnd       val
    <*> jsonIpAddress val

jsonResource :: A.Value -> Either String Resource
jsonResource val =
  maybe (Left "Missing \"Resource\"") Right $
    fmap Resource
      (val ^? key "Statement"
            . nth 0
            . key "Resource"
            . _String)

jsonStart :: A.Value -> Either String StartTime
jsonStart val =
  Right $
    maybe StartImmediately (StartTime . fromInteger)
      (val ^? key "Statement"
            . nth 0
            . key "Condition"
            . key "DateGreaterThan"
            . key "AWS:EpochTime"
            . _Integer)

jsonEnd :: A.Value -> Either String EndTime
jsonEnd val =
  maybe (Left "Missing \"DateLessThan\"") Right $
    fmap (EndTime . fromInteger)
      (val ^? key "Statement"
            . nth 0
            . key "Condition"
            . key "DateLessThan"
            . key "AWS:EpochTime"
            . _Integer)

jsonIpAddress :: A.Value -> Either String IpAddress
jsonIpAddress val =
  Right $
    maybe AnyIp IpAddress
      (val ^? key "Statement"
            . nth 0
            . key "Condition"
            . key "IpAddress"
            . key "AWS:SourceIp"
            . _String)

{- |

This function provides one convenient way to construct a simple 'Policy'.

For the full set of policy options, use the 'Policy' constructor directly.

-}

simplePolicy
  :: Resource   -- ^ URL that the policy will grant access to,
                --   optionally containing asterisks for wildcards
  -> Lifespan   -- ^ How long from now the credentials expire
  -> IO Policy

simplePolicy res life = do

  now :: POSIXTime <- getPOSIXTime

  let end = case life of Lifespan x -> EndTime (now + x)

  pure
    Policy
      { policyResource  = res
      , policyEnd       = end
      , policyStart     = StartImmediately
      , policyIpAddress = AnyIp
      }