{-# LANGUAGE OverloadedStrings #-}
-- | This module defines the kinds of permissions used in icepeak and provides
-- functions checking for sufficient permissions for certain operations.
module AccessControl
       ( AccessMode (..)
       , AuthPath (..)
       , IcepeakClaim (..)
       , Path
       , allowEverything
       , accessModeToText
       , textToAccessMode
       , isAuthorizedByClaim
       ) where

import           Data.Aeson ((.:), (.=))
import qualified Data.Aeson as Aeson
import qualified Data.List  as List
import           Data.Text  (Text)

import           Store      (Path)

-- * Claim datatypes

-- | Defines the structure of a JWT claim for Icepeak.
data IcepeakClaim = IcepeakClaim
  { icepeakClaimWhitelist :: [AuthPath]
    -- ^ The whitelist containing all authorizations.
  } deriving (Read, Show, Eq, Ord)

data AuthPath = AuthPath
  { authPathPrefix :: Path
    -- ^ The prefix of all the paths to which this authorization applies.
  , authPathModes  :: [AccessMode]
    -- ^ The modes that are authorized on this path prefix.
  } deriving (Read, Show, Eq, Ord)

-- | Different modes for accessing the JSON store
data AccessMode = ModeRead | ModeWrite
  deriving (Read, Show, Eq, Ord, Enum, Bounded)


-- | A claim that allows all operations.
allowEverything :: IcepeakClaim
allowEverything = IcepeakClaim [AuthPath [] [minBound..maxBound]]

-- * Authorization

-- | Check whether accessing the given path with the given mode is authorized by
-- the supplied claim.
isAuthorizedByClaim :: IcepeakClaim -> Path -> AccessMode -> Bool
isAuthorizedByClaim claim path mode = any allows (icepeakClaimWhitelist claim) where
  allows (AuthPath prefix modes) = List.isPrefixOf prefix path && mode `elem` modes


-- * JSON encoding and decoding

accessModeToText :: AccessMode -> Text
accessModeToText mode = case mode of
    ModeRead  -> "read"
    ModeWrite -> "write"

textToAccessMode :: Text -> Maybe AccessMode
textToAccessMode mode
  | mode == "read" = Just ModeRead
  | mode == "write" = Just ModeWrite
  | otherwise = Nothing

instance Aeson.ToJSON AccessMode where
  toJSON = Aeson.String . accessModeToText

instance Aeson.FromJSON AccessMode where
  parseJSON = Aeson.withText "mode string" $ \txt -> case textToAccessMode txt of
    Nothing -> fail "Invalid mode value."
    Just m  -> pure m

instance Aeson.ToJSON AuthPath where
  toJSON (AuthPath prefix modes) = Aeson.object
    [ "prefix" .= prefix
    , "modes" .= modes ]

instance Aeson.FromJSON AuthPath where
  parseJSON = Aeson.withObject "path and modes" $ \v -> AuthPath
    <$> v .: "prefix"
    <*> v .: "modes"

instance Aeson.ToJSON IcepeakClaim where
  toJSON claim = Aeson.object
    [ "version"   .= (1 :: Int)
    , "whitelist" .= icepeakClaimWhitelist claim
    ]

instance Aeson.FromJSON IcepeakClaim where
  parseJSON = Aeson.withObject "icepeak claim" $ \v -> do
    version <- v .: "version"
    if version == (1 :: Int)
      then IcepeakClaim <$> v .: "whitelist"
      else fail "unsupported version"