biscuit-haskell-0.2.0.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.Monad (join, void, when)
import Data.Aeson
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 Data.Foldable (traverse_)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text, pack)
import Data.Text.Encoding (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 }
data ValidationR
= ValidationR
{ world :: Maybe WorldDesc
, result :: RustResult [Text] Int
, authorizer_code :: Authorizer
} deriving stock (Eq, Show, Generic)
deriving anyclass FromJSON
checkResult :: Show a
=> RustResult [Text] Int
-> Either a b
-> Assertion
checkResult 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
_ -> 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
parseErrorToRust :: ParseError -> RustResult [Text] a
parseErrorToRust = Err . pure . \case
InvalidHexEncoding -> "todo"
InvalidB64Encoding -> "todo"
InvalidProtobufSer w e -> pack $ "todo " <> show w <> e
InvalidProtobuf True "Invalid signature" -> "Format(InvalidSignatureSize(16))"
InvalidProtobuf w e -> "todo"
InvalidProof -> "todo"
InvalidSignatures -> "Format(Signature(InvalidSignature(\"signature error\")))"
processFailedValidation :: (String -> IO ())
-> ParseError
-> (String, ValidationR)
-> Assertion
processFailedValidation step e (name, ValidationR{result}) = do
step $ "Checking validation " <> name
checkResult result (Left e)
execErrorToRust :: Either ExecutionError a -> RustResult [Text] Int
execErrorToRust (Right _) = Ok 0
execErrorToRust (Left e) = Err $ case e of
Timeout -> ["todo"]
TooManyFacts -> ["todo"]
TooManyIterations -> ["todo"]
FactsInBlocks -> ["todo"]
ResultError (NoPoliciesMatched cs) -> ["todo"]
ResultError (FailedChecks cs) -> ["Block(FailedBlockCheck { block_id: 1, check_id: 0, rule: \"check if resource($0), operation(#read), right($0, #read)\" })"]
ResultError (DenyRuleMatched cs q) -> ["todo"]
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 result res
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