biscuit-haskell-0.2.1.0: test/Spec/SampleReader.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Spec.SampleReader where
import Debug.Trace
import Control.Arrow ((&&&))
import Control.Lens ((^?))
import Control.Monad (join, void, when)
import Data.Aeson
import Data.Aeson.Lens (key)
import Data.Aeson.Types (typeMismatch, unexpected)
import Data.Attoparsec.Text (parseOnly)
import Data.Bifunctor (Bifunctor (..))
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base16 as Hex
import qualified Data.ByteString.Lazy as LBS
import Data.Foldable (traverse_)
import Data.List.NonEmpty (NonEmpty (..), toList)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (isJust, isNothing)
import Data.Text (Text, pack, unpack)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import GHC.Generics (Generic)
import Test.Tasty hiding (Timeout)
import Test.Tasty.HUnit
import Auth.Biscuit
import Auth.Biscuit.Datalog.Executor (ExecutionError (..),
ResultError (..))
import Auth.Biscuit.Datalog.Parser (authorizerParser, blockParser)
import Auth.Biscuit.Token
getB :: ParsedSignedBlock -> Block
getB ((_, b), _, _) = b
getAuthority :: Biscuit OpenOrSealed Verified -> Block
getAuthority = getB . authority
getBlocks :: Biscuit OpenOrSealed Verified -> [Block]
getBlocks = fmap getB . blocks
instance FromJSON SecretKey where
parseJSON = withText "Ed25519 secret key" $ \t -> do
let bs = encodeUtf8 t
res = parseSecretKeyHex bs
notSk = typeMismatch "Ed25519 secret key" (String t)
maybe notSk pure res
instance FromJSON PublicKey where
parseJSON = withText "Ed25519 public key" $ \t -> do
let bs = encodeUtf8 t
res = parsePublicKeyHex bs
notPk = typeMismatch "Ed25519 public key" (String t)
maybe notPk pure res
instance FromJSON Authorizer where
parseJSON = withText "authorizer" $ \t -> do
let res = parseAuthorizer t
notAuthorizer e = typeMismatch e (String t)
either notAuthorizer pure res
parseAuthorizer :: Text -> Either String Authorizer
parseAuthorizer = parseOnly authorizerParser
parseBlock :: Text -> Either String Block
parseBlock = parseOnly blockParser
data SampleFile a
= SampleFile
{ root_private_key :: SecretKey
, root_public_key :: PublicKey
, testcases :: [TestCase a]
}
deriving stock (Eq, Show, Generic, Functor, Foldable, Traversable)
deriving anyclass FromJSON
data RustResult e a
= Err e
| Ok a
deriving stock (Generic, Eq, Show)
instance Bifunctor RustResult where
bimap f g = \case
Err e -> Err $ f e
Ok a -> Ok $ g a
instance (FromJSON e, FromJSON a) => FromJSON (RustResult e a) where
parseJSON = genericParseJSON $
defaultOptions { sumEncoding = ObjectWithSingleField }
type RustError = Value
data ValidationR
= ValidationR
{ world :: Maybe WorldDesc
, result :: RustResult RustError Int
, authorizer_code :: Authorizer
, revocation_ids :: [Text]
} deriving stock (Eq, Show, Generic)
deriving anyclass FromJSON
checkResult :: Show a
=> (a -> RustError -> Assertion)
-> RustResult RustError Int
-> Either a b
-> Assertion
checkResult f r e = case (r, e) of
(Err es, Right _) -> assertFailure $ "Got success, but expected failure: " <> show es
(Ok _, Left e) -> assertFailure $ "Expected success, but got failure: " <> show e
(Err es, Left e) -> f e es
_ -> pure ()
data TestCase a
= TestCase
{ title :: String
, filename :: a
, token :: NonEmpty BlockDesc
, validations :: Map String ValidationR
}
deriving stock (Eq, Show, Generic, Functor, Foldable, Traversable)
deriving anyclass FromJSON
data BlockDesc
= BlockDesc
{ symbols :: [Text]
, code :: Text
}
deriving stock (Eq, Show, Generic)
deriving anyclass FromJSON
data WorldDesc
= WorldDesc
{ facts :: [Text]
, rules :: [Text]
, checks :: [Text]
, policies :: [Text]
}
deriving stock (Eq, Show, Generic)
deriving anyclass FromJSON
readBiscuits :: SampleFile FilePath -> IO (SampleFile (FilePath, ByteString))
readBiscuits =
traverse $ traverse (BS.readFile . ("test/samples/v2/" <>)) . join (&&&) id
readSamplesFile :: IO (SampleFile (FilePath, ByteString))
readSamplesFile = do
Just f <- decodeFileStrict' "test/samples/v2/samples.json"
readBiscuits f
checkTokenBlocks :: (String -> IO ())
-> Biscuit OpenOrSealed Verified
-> NonEmpty BlockDesc
-> Assertion
checkTokenBlocks step b blockDescs = do
step "Checking blocks"
let bs = getAuthority b :| getBlocks b
expected = traverse (parseBlock . code) blockDescs
expected @?= Right bs
processTestCase :: (String -> IO ())
-> PublicKey -> TestCase (FilePath, ByteString)
-> Assertion
processTestCase step rootPk TestCase{..} = do
step "Parsing "
let vList = Map.toList validations
case parse rootPk (snd filename) of
Left parseError -> traverse_ (processFailedValidation step parseError) vList
Right biscuit -> do
checkTokenBlocks step biscuit token
traverse_ (processValidation step biscuit) vList
compareParseErrors :: ParseError -> RustError -> Assertion
compareParseErrors pe re =
let mustMatch p = assertBool (show re) $ isJust $ re ^? p
in case pe of
InvalidHexEncoding ->
assertFailure $ "InvalidHexEncoding can't appear here " <> show re
InvalidB64Encoding ->
mustMatch $ key "Base64"
InvalidProtobufSer True s ->
mustMatch $ key "Format" . key "SerializationError"
InvalidProtobufSer False s ->
mustMatch $ key "Format" . key "BlockSerializationError"
InvalidProtobuf True "Invalid signature" ->
mustMatch $ key "Format" . key "InvalidSignatureSize"
InvalidProtobuf True s ->
mustMatch $ key "Format" . key "DeserializationError"
InvalidProtobuf False s ->
mustMatch $ key "Format" . key "BlockDeserializationError"
InvalidSignatures ->
mustMatch $ key "Format" . key "Signature" . key "InvalidSignature"
InvalidProof ->
assertFailure $ "InvalidProof can't appear here " <> show re
RevokedBiscuit ->
assertFailure $ "RevokedBiscuit can't appear here " <> show re
compareExecErrors :: ExecutionError -> RustError -> Assertion
compareExecErrors ee re =
let errorMessage = "ExecutionError mismatch: " <> show ee <> " " <> unpack (decodeUtf8 . LBS.toStrict $ encode re)
mustMatch p = assertBool errorMessage $ isJust $ re ^? p
-- todo compare `Unauthorized` contents
in case ee of
Timeout -> mustMatch $ key "RunLimit" . key "Timeout"
TooManyFacts -> mustMatch $ key "RunLimit" . key "TooManyFacts"
TooManyIterations -> mustMatch $ key "RunLimit" . key "TooManyIterations"
InvalidRule -> mustMatch $ key "FailedLogic" . key "InvalidBlockRule"
ResultError (NoPoliciesMatched cs) -> mustMatch $ key "FailedLogic" . key "Unauthorized"
ResultError (FailedChecks cs) -> mustMatch $ key "FailedLogic" . key "Unauthorized"
ResultError (DenyRuleMatched cs q) -> mustMatch $ key "FailedLogic" . key "Unauthorized"
processFailedValidation :: (String -> IO ())
-> ParseError
-> (String, ValidationR)
-> Assertion
processFailedValidation step e (name, ValidationR{result}) = do
step $ "Checking validation " <> name
checkResult compareParseErrors result (Left e)
processValidation :: (String -> IO ())
-> Biscuit OpenOrSealed Verified
-> (String, ValidationR)
-> Assertion
processValidation step b (name, ValidationR{..}) = do
when (name /= "") $ step ("Checking " <> name)
w <- maybe (assertFailure "missing authorizer contents") pure world
pols <- either (assertFailure . show) pure $ parseAuthorizer $ foldMap (<> ";") (policies w)
res <- authorizeBiscuit b (authorizer_code <> pols)
checkResult compareExecErrors result res
let revocationIds = decodeUtf8 . Hex.encode <$> toList (getRevocationIds b)
step "Comparing revocation ids"
revocation_ids @?= revocationIds
runTests :: (String -> IO ())
-> Assertion
runTests step = do
step "Parsing sample file"
SampleFile{..} <- readSamplesFile
traverse_ (processTestCase step root_public_key) testcases
specs :: TestTree
specs = testCaseSteps "Biscuit samples - compliance checks" runTests
mkTestCase :: PublicKey -> TestCase (FilePath, ByteString) -> TestTree
mkTestCase root_public_key tc@TestCase{filename} =
testCaseSteps (fst filename) (\step -> processTestCase step root_public_key tc)
getSpecs :: IO TestTree
getSpecs = do
SampleFile{..} <- readSamplesFile
pure $ testGroup "Biscuit samples - compliance checks"
$ mkTestCase root_public_key <$> testcases