packages feed

biscuit-haskell-0.3.0.0: test/Spec/Parser.hs

{-# LANGUAGE DataKinds         #-}
{-# LANGUAGE OverloadedLists   #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}
module Spec.Parser (specs, parseExpression, parseBlock, parseAuthorizer) where

import           Control.Monad               ((<=<))
import           Data.Bifunctor              (first)
import           Data.Either                 (isLeft)
import           Data.List.NonEmpty          (NonEmpty)
import           Data.Maybe                  (fromJust)
import qualified Data.Set                    as Set
import           Data.Text                   (Text)
import           Test.Tasty
import           Test.Tasty.HUnit
import           Validation                  (Validation, validationToEither)

import           Auth.Biscuit                (PublicKey, parsePublicKeyHex)
import           Auth.Biscuit.Datalog.AST
import           Auth.Biscuit.Datalog.Parser (Parser, authorizerParser,
                                              blockParser, checkParser,
                                              expressionParser, policyParser,
                                              predicateParser,
                                              predicateTermParser, ruleParser,
                                              run)

pk1, pk2, pk3, pk4, pk5 :: PublicKey
pk1 = fromJust $ parsePublicKeyHex "a1b712761c609039f878edad694d762652f1548a68acccc96735b3196a240e8b"
pk2 = fromJust $ parsePublicKeyHex "b82c748be51784a58496675752e04cc48009a7e78bcfae8cad51fba959102af1"
pk3 = fromJust $ parsePublicKeyHex "083aae4ba29a9a3781cdee7a800f4f8ab90591f65ca983fc429687628311aedd"
pk4 = fromJust $ parsePublicKeyHex "c6864578bc03596d52878bd70025ec966c95c60727cb6573198453e82132510d"
pk5 = fromJust $ parsePublicKeyHex "a0d3dc7ab62a0a2732ba267e0d57894170458ec1659ca1226240b99764554a2e"

runWithNoParams :: Parser a
                -> (a -> Validation (NonEmpty Text) b)
                -> Text
                -> Either String b
runWithNoParams p s = validationToEither . first show . s <=< run p

parseTerm :: Text -> Either String Term
parseTerm = runWithNoParams predicateTermParser $ substitutePTerm mempty

parseTermQQ :: Text -> Either String QQTerm
parseTermQQ = run predicateTermParser

parsePredicate :: Text -> Either String Predicate
parsePredicate = runWithNoParams predicateParser $ substitutePredicate mempty

parseRule :: Text -> Either String Rule
parseRule = runWithNoParams (ruleParser False) $ substituteRule mempty mempty

parseExpression :: Text -> Either String Expression
parseExpression = runWithNoParams expressionParser $ substituteExpression mempty

parseCheck :: Text -> Either String Check
parseCheck = runWithNoParams (checkParser False) $ substituteCheck mempty mempty

parseAuthorizer :: Text -> Either String Authorizer
parseAuthorizer = runWithNoParams authorizerParser $ substituteAuthorizer mempty mempty

parsePolicy :: Text -> Either String Policy
parsePolicy = runWithNoParams policyParser $ substitutePolicy mempty mempty

parseBlock :: Text -> Either String Block
parseBlock = runWithNoParams blockParser $ substituteBlock mempty mempty

specs :: TestTree
specs = testGroup "datalog parser"
  [ factWithDate
  , simpleFact
  , oneLetterFact
  , simpleRule
  , multilineRule
  , termsGroup
  , termsGroupQQ
  , constraints
  , constrainedRule
  , constrainedRuleOrdering
  , ruleVariables
  , ruleWithScopeParsing
  , checkParsing
  , policyParsing
  , authorizerParsing
  , blockParsing
  ]

termsGroup :: TestTree
termsGroup = testGroup "Parse terms"
  [ testCase "String" $ parseTerm "\"file1 a hello - 123_\"" @?= Right (LString "file1 a hello - 123_")
  , testCase "Positive integer" $ parseTerm "123" @?= Right (LInteger 123)
  , testCase "Negative integer" $ parseTerm "-42" @?= Right (LInteger (-42))
  , testCase "Date" $ parseTerm "2019-12-02T13:49:53Z" @?=
        Right (LDate $ read "2019-12-02 13:49:53 UTC")
  , testCase "Variable" $ parseTerm "$1" @?= Right (Variable "1")
  , testCase "Antiquote" $ isLeft (parseTerm "{toto}") @? "Unsubstituted parameters triggers an error"
  ]

termsGroupQQ :: TestTree
termsGroupQQ = testGroup "Parse terms (in a QQ setting)"
  [ testCase "String" $ parseTermQQ "\"file1 a hello - 123_\"" @?= Right (LString "file1 a hello - 123_")
  , testCase "Positive integer" $ parseTermQQ "123" @?= Right (LInteger 123)
  , testCase "Negative integer" $ parseTermQQ "-42" @?= Right (LInteger (-42))
  , testCase "Date" $ parseTermQQ "2019-12-02T13:49:53Z" @?=
        Right (LDate $ read "2019-12-02 13:49:53 UTC")
  , testCase "Variable" $ parseTermQQ "$1" @?= Right (Variable "1")
  , testGroup "Antiquote"
     [ testCase "Variable name" $ parseTermQQ "{toto2_'}" @?= Right (Antiquote "toto2_'")
     , testCase "Leading underscore" $ parseTermQQ "{_toto}" @?= Right (Antiquote "_toto")
     , testCase "`_` is reserved" $ parseTermQQ "{_}" @?= Left "1:3:\n  |\n1 | {_}\n  |   ^\nunexpected '}'\nexpecting letter\n"
     , testCase "Variables are lower-cased" $ parseTermQQ "{Toto}" @?= Left "1:2:\n  |\n1 | {Toto}\n  |  ^\nunexpected 'T'\nexpecting '_' or lowercase letter\n"
     , testCase "_ is lower-case" $ parseTermQQ "{_Toto}" @?= Right (Antiquote "_Toto")
     , testCase "unicode is allowed" $ parseTermQQ "{éllo}" @?= Right (Antiquote "éllo")
     ]
  ]

simpleFact :: TestTree
simpleFact = testCase "Parse simple fact" $
  parsePredicate "right(\"file1\", \"read\")" @?=
    Right (Predicate "right" [LString "file1", LString "read"])

oneLetterFact :: TestTree
oneLetterFact = testCase "Parse one-letter fact" $
  parsePredicate "a(12)" @?=
    Right (Predicate "a" [LInteger 12])

factWithDate :: TestTree
factWithDate = testCase "Parse fact containing a date" $ do
  parsePredicate "date(2019-12-02T13:49:53Z)" @?=
    Right (Predicate "date" [LDate $ read "2019-12-02 13:49:53 UTC"])
  parsePredicate "date(2019-12-02T13:49:53.001Z)" @?=
    Right (Predicate "date" [LDate $ read "2019-12-02 13:49:53.001 UTC"])
  parsePredicate "date(2019-12-02T13:49:53+00:00)" @?=
    Right (Predicate "date" [LDate $ read "2019-12-02 13:49:53 UTC"])

simpleRule :: TestTree
simpleRule = testCase "Parse simple rule" $
  parseRule "right($0, \"read\") <- resource( $0), operation(\"read\")" @?=
    Right (Rule (Predicate "right" [Variable "0", LString "read"])
                [ Predicate "resource" [Variable "0"]
                , Predicate "operation" [LString "read"]
                ] [] [])

multilineRule :: TestTree
multilineRule = testCase "Parse multiline rule" $
  parseRule "right($0, \"read\") <-\n resource( $0),\n operation(\"read\")" @?=
    Right (Rule (Predicate "right" [Variable "0", LString "read"])
                [ Predicate "resource" [Variable "0"]
                , Predicate "operation" [LString "read"]
                ] [] [])

constrainedRule :: TestTree
constrainedRule = testCase "Parse constained rule" $
  parseRule "valid_date(\"file1\") <- time($0), resource(\"file1\"), $0 <= 2019-12-04T09:46:41+00:00" @?=
    Right (Rule (Predicate "valid_date" [LString "file1"])
                [ Predicate "time" [Variable "0"]
                , Predicate "resource" [LString "file1"]
                ]
                [ EBinary LessOrEqual
                    (EValue $ Variable "0")
                    (EValue $ LDate $ read "2019-12-04 09:46:41 UTC")
                ] [])

constrainedRuleOrdering :: TestTree
constrainedRuleOrdering = testCase "Parse constained rule (interleaved)" $
  parseRule "valid_date(\"file1\") <- time($0), $0 <= 2019-12-04T09:46:41+00:00, resource(\"file1\")" @?=
    Right (Rule (Predicate "valid_date" [LString "file1"])
                [ Predicate "time" [Variable "0"]
                , Predicate "resource" [LString "file1"]
                ]
                [ EBinary LessOrEqual
                    (EValue $ Variable "0")
                    (EValue $ LDate $ read "2019-12-04 09:46:41 UTC")
                ] [])

constraints :: TestTree
constraints = testGroup "Parse expressions"
  [ testCase "date comparison (LTE)" $
      parseExpression "$0 <= 2030-12-31T12:59:59+00:00" @?=
        Right (EBinary LessOrEqual
                 (EValue (Variable "0"))
                 (EValue (LDate $ read "2030-12-31 12:59:59 UTC"))
                 )
  , testCase "date comparison (GTE)" $
      parseExpression "$0 >= 2030-12-31T12:59:59+00:00" @?=
        Right (EBinary GreaterOrEqual
                 (EValue (Variable "0"))
                 (EValue (LDate $ read "2030-12-31 12:59:59 UTC"))
                 )
  , testCase "int comparison (LT)" $
      parseExpression "$0 < 1234" @?=
        Right (EBinary LessThan
                 (EValue (Variable "0"))
                 (EValue (LInteger 1234))
                 )
  , testCase "int comparison (GT)" $
      parseExpression "$0 > 1234" @?=
        Right (EBinary GreaterThan
                 (EValue (Variable "0"))
                 (EValue (LInteger 1234))
                 )
  , testCase "int comparison (LTE)" $
      parseExpression "$0 <= 1234" @?=
        Right (EBinary LessOrEqual
                 (EValue (Variable "0"))
                 (EValue (LInteger 1234))
                 )
  , testCase "int comparison (EQ)" $
      parseExpression "$0 == 1" @?=
        Right (EBinary Equal
                 (EValue (Variable "0"))
                 (EValue (LInteger 1))
                 )
  , testCase "negative int comparison (GTE)" $
      parseExpression "$0 >= -1234" @?=
        Right (EBinary GreaterOrEqual
                 (EValue (Variable "0"))
                 (EValue (LInteger (-1234)))
                 )
  , testCase "string comparison" $
      parseExpression "$0 == \"abc\"" @?=
        Right (EBinary Equal
                 (EValue (Variable "0"))
                 (EValue (LString "abc"))
                 )
  , testCase "string comparison (starts_with)" $
      parseExpression "$0.starts_with(\"abc\")" @?=
        Right (EBinary Prefix
                 (EValue (Variable "0"))
                 (EValue (LString "abc"))
                 )
  , testCase "string comparison (ends_with)" $
      parseExpression "$0.ends_with(\"abc\")" @?=
        Right (EBinary Suffix
                 (EValue (Variable "0"))
                 (EValue (LString "abc"))
                 )
  , testCase "string comparison (matches)" $
      parseExpression "$0.matches(\"abc\")" @?=
        Right (EBinary Regex
                 (EValue (Variable "0"))
                 (EValue (LString "abc"))
                 )
  , testCase "int set operation" $
      parseExpression "[1, 2].contains($0)" @?=
        Right (EBinary Contains
                 (EValue (TermSet $ Set.fromList [LInteger 1, LInteger 2]))
                 (EValue (Variable "0"))
                 )
  , testCase "negated int set operation" $
      parseExpression "![1, 2].contains($0)" @?=
        Right (EUnary Negate
                 (EBinary Contains
                    (EValue (TermSet $ Set.fromList [LInteger 1, LInteger 2]))
                    (EValue (Variable "0"))
                    ))
  , testCase "string set operation" $
      parseExpression "[\"abc\", \"def\"].contains($0)" @?=
        Right (EBinary Contains
                 (EValue (TermSet $ Set.fromList [LString "abc", LString "def"]))
                 (EValue (Variable "0"))
                 )
  , testCase "negated string set operation" $
      parseExpression "![\"abc\", \"def\"].contains($0)" @?=
        Right (EUnary Negate
                 (EBinary Contains
                    (EValue (TermSet $ Set.fromList [LString "abc", LString "def"]))
                    (EValue (Variable "0"))
                    ))
  , testCase "arithmetic operation that looks like the beginning of a RFC3339 date" $
      parseExpression "2022-12-10==2000" @?=
        Right (EBinary Equal
                 (EBinary Sub
                    (EBinary Sub
                       (EValue $ LInteger 2022)
                       (EValue $ LInteger 12)
                    )
                    (EValue $ LInteger 10)
                 )
                 (EValue $ LInteger 2000)
              )
  , operatorPrecedences
  ]

operatorPrecedences :: TestTree
operatorPrecedences = testGroup "mixed-precedence operators"
  [ testCase "< +" $
      parseExpression " 1 < $test + 2 " @?=
        Right (EBinary LessThan
                 (EValue $ LInteger 1)
                 (EBinary Add
                    (EValue $ Variable "test")
                    (EValue $ LInteger 2)
                 )
              )
  , testCase "< && starts_with" $
      parseExpression " 2 < $test && $var2.starts_with(\"test\") && true " @?=
        Right (EBinary And
                 (EBinary And
                    (EBinary LessThan
                       (EValue $ LInteger 2)
                       (EValue $ Variable "test")
                    )
                    (EBinary Prefix
                       (EValue $ Variable "var2")
                       (EValue $ LString "test")
                    )
                 )
                 (EValue $ LBool True)
              )
  , testCase "+ *" $
      parseExpression "1 + 2 * 3" @?=
        Right (EBinary Add
                 (EValue $ LInteger 1)
                 (EBinary Mul
                    (EValue $ LInteger 2)
                    (EValue $ LInteger 3)
                 )
              )
  , testCase "+ * parens" $
      parseExpression "(1 + 2) * 3" @?=
        Right (EBinary Mul
                 (EUnary Parens
                    (EBinary Add
                       (EValue $ LInteger 1)
                       (EValue $ LInteger 2)
                    )
                 )
                 (EValue $ LInteger 3)
              )
  , testCase "+ & | *" $
      parseExpression "1 + 2 | 4 * 3 & 4" @?=
        Right (EBinary BitwiseOr
                  (EBinary Add
                    (EValue $ LInteger 1)
                    (EValue $ LInteger 2)
                  )
                  (EBinary BitwiseAnd
                    (EBinary Mul
                      (EValue $ LInteger 4)
                      (EValue $ LInteger 3)
                    )
                    (EValue $ LInteger 4)
                  )
              )
  ]

ruleVariables :: TestTree
ruleVariables = testGroup "Make sure rule & query variables are correctly introduced"
  [ testCase "Head variables are correctly introduced" $
      parseRule "head($unbound) <- body(true)" @?=
        Left "1:1:\n  |\n1 | head($unbound) <- body(true)\n  | ^\nUnbound variables: unbound\n"
  , testCase "Expression variables are correctly introduced (rule)" $
      parseRule "head(true) <- body(true), $unbound" @?=
        Left "1:1:\n  |\n1 | head(true) <- body(true), $unbound\n  | ^\nUnbound variables: unbound\n"
  , testCase "Expression variables are correctly introduced (check)" $
      parseCheck "check if body(true), $unbound" @?=
        Left "1:10:\n  |\n1 | check if body(true), $unbound\n  |          ^\nUnbound variables: unbound\n"
  , testCase "Expression variables are correctly introduced (allow policy)" $
      parsePolicy "allow if body(true), $unbound" @?=
        Left "1:10:\n  |\n1 | allow if body(true), $unbound\n  |          ^\nUnbound variables: unbound\n"
  , testCase "Expression variables are correctly introduced (deny policy)" $
      parsePolicy "deny if body(true), $unbound" @?=
        Left "1:9:\n  |\n1 | deny if body(true), $unbound\n  |         ^\nUnbound variables: unbound\n"
  ]

ruleWithScopeParsing :: TestTree
ruleWithScopeParsing = testCase "Parse constained rule with scope annotation" $
  parseRule "valid_date(\"file1\") <- time($0), resource(\"file1\"), $0 <= 2019-12-04T09:46:41+00:00 trusting previous" @?=
    Right (Rule (Predicate "valid_date" [LString "file1"])
                [ Predicate "time" [Variable "0"]
                , Predicate "resource" [LString "file1"]
                ]
                [ EBinary LessOrEqual
                    (EValue $ Variable "0")
                    (EValue $ LDate $ read "2019-12-04 09:46:41 UTC")
                ] [Previous])

checkParsing :: TestTree
checkParsing = testGroup "check blocks"
  [ testCase "Simple check" $
      parseCheck "check if true" @?=
        Right Check
          { cQueries = [QueryItem [] [EValue $ LBool True] []]
          , cKind = One
          }
  , testCase "Simple check all" $
      parseCheck "check all true" @?=
        Right Check
          { cQueries = [QueryItem [] [EValue $ LBool True] []]
          , cKind = All
          }
  , testCase "Multiple groups" $
      parseCheck
        "check if fact($var), $var == true or \
        \other($var), $var == 2" @?=
          Right Check
            { cQueries =
                [ QueryItem [Predicate "fact" [Variable "var"]]
                            [EBinary Equal (EValue (Variable "var")) (EValue (LBool True))]
                            []
                , QueryItem [Predicate "other" [Variable "var"]]
                            [EBinary Equal (EValue (Variable "var")) (EValue (LInteger 2))]
                            []
                ]
            , cKind = One
            }
  , testCase "Multiple check all groups" $
      parseCheck
        "check all fact($var), $var == true or \
        \other($var), $var == 2" @?=
          Right Check
            { cQueries =
                [ QueryItem [Predicate "fact" [Variable "var"]]
                            [EBinary Equal (EValue (Variable "var")) (EValue (LBool True))]
                            []
                , QueryItem [Predicate "other" [Variable "var"]]
                            [EBinary Equal (EValue (Variable "var")) (EValue (LInteger 2))]
                            []
                ]
            , cKind = All
            }
  , testCase "Multiple groups, scoped" $
      parseCheck
        "check if fact($var), $var == true trusting previous or \
        \other($var), $var == 2 trusting authority" @?=
          Right Check
            { cQueries =
                [ QueryItem [Predicate "fact" [Variable "var"]]
                            [EBinary Equal (EValue (Variable "var")) (EValue (LBool True))]
                            [Previous]
                , QueryItem [Predicate "other" [Variable "var"]]
                            [EBinary Equal (EValue (Variable "var")) (EValue (LInteger 2))]
                            [OnlyAuthority]
                ]
            , cKind = One
            }
  , testCase "Multiple check all groups, scoped" $
      parseCheck
        "check all fact($var), $var == true trusting previous or \
        \other($var), $var == 2 trusting authority" @?=
          Right Check
            { cQueries =
                [ QueryItem [Predicate "fact" [Variable "var"]]
                            [EBinary Equal (EValue (Variable "var")) (EValue (LBool True))]
                            [Previous]
                , QueryItem [Predicate "other" [Variable "var"]]
                            [EBinary Equal (EValue (Variable "var")) (EValue (LInteger 2))]
                            [OnlyAuthority]
                ]
            , cKind = All
            }
  ]

policyParsing :: TestTree
policyParsing = testGroup "policy blocks"
  [ testCase "Simple allow policy" $
      parsePolicy "allow if true" @?=
        Right (Allow, [QueryItem [] [EValue $ LBool True] []])
  , testCase "Simple deny policy" $
      parsePolicy "deny if true" @?=
        Right (Deny, [QueryItem [] [EValue $ LBool True] []])
  , testCase "Allow with multiple groups" $
      parsePolicy
        "allow if fact($var), $var == true or \
        \other($var), $var == 2" @?=
          Right
            ( Allow
            , [ QueryItem [Predicate "fact" [Variable "var"]]
                          [EBinary Equal (EValue (Variable "var")) (EValue (LBool True))]
                          []
              , QueryItem [Predicate "other" [Variable "var"]]
                          [EBinary Equal (EValue (Variable "var")) (EValue (LInteger 2))]
                          []
              ]
            )
  , testCase "Deny with multiple groups" $
      parsePolicy
        "deny if fact($var), $var == true or \
        \other($var), $var == 2" @?=
          Right
            ( Deny
            , [ QueryItem [Predicate "fact" [Variable "var"]]
                          [EBinary Equal (EValue (Variable "var")) (EValue (LBool True))]
                          []
              , QueryItem [Predicate "other" [Variable "var"]]
                          [EBinary Equal (EValue (Variable "var")) (EValue (LInteger 2))]
                          []
              ]
            )
  , testCase "Deny with multiple groups, multiline" $
      parsePolicy
        "deny if\n\
           \fact($var), $var == true or //comment\n\
           \other($var), $var == 2" @?=
          Right
            ( Deny
            , [ QueryItem [Predicate "fact" [Variable "var"]]
                          [EBinary Equal (EValue (Variable "var")) (EValue (LBool True))]
                          []
              , QueryItem [Predicate "other" [Variable "var"]]
                          [EBinary Equal (EValue (Variable "var")) (EValue (LInteger 2))]
                          []
              ]
            )
  , testCase "Allow with multiple groups, scoped" $
      parsePolicy
        "allow if fact($var), $var == true trusting authority or \
        \other($var), $var == 2 trusting ed25519/a1b712761c609039f878edad694d762652f1548a68acccc96735b3196a240e8b,ed25519/083aae4ba29a9a3781cdee7a800f4f8ab90591f65ca983fc429687628311aedd,ed25519/c6864578bc03596d52878bd70025ec966c95c60727cb6573198453e82132510d " @?=
          Right
            ( Allow
            , [ QueryItem [Predicate "fact" [Variable "var"]]
                          [EBinary Equal (EValue (Variable "var")) (EValue (LBool True))]
                          [OnlyAuthority]
              , QueryItem [Predicate "other" [Variable "var"]]
                          [EBinary Equal (EValue (Variable "var")) (EValue (LInteger 2))]
                          [BlockId pk1, BlockId pk3, BlockId pk4]
              ]
            )
  ]

authorizerParsing :: TestTree
authorizerParsing = testGroup "Simple authorizers"
  [ testCase "Just a deny" $
      parseAuthorizer "deny if true;" @?=
        Right (Authorizer [(Deny, [QueryItem [] [EValue (LBool True)] []])] mempty
              )
  , testCase "Allow and deny" $
      parseAuthorizer "allow if operation(\"read\");\n deny if true;" @?=
        Right (Authorizer
                 [  (Allow, [QueryItem [Predicate "operation" [LString "read"]] [] []])
                 , (Deny, [QueryItem [] [EValue (LBool True)] []])
                 ]
                 mempty
              )
  , testCase "Previous is denied at the top level" $
      parseAuthorizer "trusting previous; allow if true;" @?=
        Left "1:10:\n  |\n1 | trusting previous; allow if true;\n  |          ^\n'previous' can't appear in an authorizer scope\n"
  , testCase "Previous is denied in rules" $
      parseAuthorizer "head(true) <- tail(true) trusting previous;" @?=
        Left "1:35:\n  |\n1 | head(true) <- tail(true) trusting previous;\n  |                                   ^\n'previous' can't appear in an authorizer scope\n"
  , testCase "Previous is denied in checks" $
      parseAuthorizer "check if true trusting previous;" @?=
        Left "1:24:\n  |\n1 | check if true trusting previous;\n  |                        ^\n'previous' can't appear in an authorizer scope\n"
  , testCase "Previous is denied in policies" $
      parseAuthorizer "allow if true trusting previous;" @?=
        Left "1:24:\n  |\n1 | allow if true trusting previous;\n  |                        ^\n'previous' can't appear in an authorizer scope\n"
  , testCase "Complete authorizer" $ do
      let spec :: Text
          spec =
            " trusting authority;\n\
            \// the owner has all rights\n\
            \right($blog_id, $article_id, $operation) <-\n\
            \    article($blog_id, $article_id),\n\
            \    operation($operation),\n\
            \    user($user_id),\n\
            \    owner($user_id, $blog_id);\n\
            \// premium users can access some restricted articles\n\
            \right($blog_id, $article_id, \"read\") <-\n\
            \  article($blog_id, $article_id),\n\
            \  premium_readable($blog_id, $article_id),\n\
            \  user($user_id),\n\
            \  premium_user($user_id, $blog_id);\n\
            \// define teams and roles\n\
            \right($blog_id, $article_id, $operation) <-\n\
            \  article($blog_id, $article_id),\n\
            \  operation($operation),\n\
            \  user($user_id),\n\
            \  member($user_id, $team_id),\n\
            \  team_role($team_id, $blog_id, \"contributor\"),\n\
            \  [\"read\", \"write\"].contains($operation);\n\
            \// unauthenticated users have read access on published articles\n\
            \allow if\n\
            \  operation(\"read\"),\n\
            \  article($blog_id, $article_id),\n\
            \  readable($blog_id, $article_id);\n\
            \// authorize if got the rights on this blog and article\n\
            \allow if\n\
            \  blog($blog_id),\n\
            \  article($blog_id, $article_id),\n\
            \  operation($operation),\n\
            \  right($blog_id, $article_id, $operation);\n\
            \// catch all rule in case the allow did not match\n\
            \deny if true;\
            \ "
          p = Predicate
          sRead = LString "read"
          sWrite = LString "write"
          sContributor = LString "contributor"
          vBlogId = Variable "blog_id"
          vArticleId = Variable "article_id"
          vUserId = Variable "user_id"
          vTeamId = Variable "team_id"
          vOp = Variable "operation"
          bRules =
            [ Rule (p "right" [vBlogId, vArticleId, vOp])
                   [ p "article" [vBlogId, vArticleId]
                   , p "operation" [vOp]
                   , p "user" [vUserId]
                   , p "owner" [vUserId, vBlogId]
                   ] [] []
            , Rule (p "right" [vBlogId, vArticleId, sRead])
                   [ p "article" [vBlogId, vArticleId]
                   , p "premium_readable" [vBlogId, vArticleId]
                   , p "user" [vUserId]
                   , p "premium_user" [vUserId, vBlogId]
                   ] [] []
            , Rule (p "right" [vBlogId, vArticleId, vOp])
                   [ p "article" [vBlogId, vArticleId]
                   , p "operation" [vOp]
                   , p "user" [vUserId]
                   , p "member" [vUserId, vTeamId]
                   , p "team_role" [vTeamId, vBlogId, sContributor]
                   ] [EBinary Contains (EValue (TermSet $ Set.fromList [sRead, sWrite]))
                                       (EValue vOp)] []
           ]
          bFacts = []
          bChecks = []
          bContext = Nothing
          bScope = [OnlyAuthority]
          vPolicies =
            [ (Allow, [QueryItem [ p "operation" [sRead]
                                 , p "article"   [vBlogId, vArticleId]
                                 , p "readable"  [vBlogId, vArticleId]
                                 ] [] []])
            , (Allow, [QueryItem [ p "blog" [vBlogId]
                                 , p "article" [vBlogId, vArticleId]
                                 , p "operation" [vOp]
                                 , p "right" [vBlogId, vArticleId, vOp]
                                 ] [] []])
            , (Deny, [QueryItem [] [EValue (LBool True)] []])
            ]
      parseAuthorizer spec @?= Right Authorizer{vBlock = Block{..}, ..}
  ]

blockParsing :: TestTree
blockParsing = testCase "Full block" $ do
  let spec :: Text
      spec =
        " trusting ed25519/b82c748be51784a58496675752e04cc48009a7e78bcfae8cad51fba959102af1,ed25519/083aae4ba29a9a3781cdee7a800f4f8ab90591f65ca983fc429687628311aedd,ed25519/a0d3dc7ab62a0a2732ba267e0d57894170458ec1659ca1226240b99764554a2e;\n\
        \// the owner has all rights\n\
        \right($blog_id, $article_id, $operation) <-\n\
        \    article($blog_id, $article_id),\n\
        \    operation($operation),\n\
        \    user($user_id),\n\
        \    owner($user_id, $blog_id);\n\
        \// premium users can access some restricted articles\n\
        \right($blog_id, $article_id, \"read\") <-\n\
        \  article($blog_id, $article_id),\n\
        \  premium_readable($blog_id, $article_id),\n\
        \  user($user_id),\n\
        \  premium_user($user_id, $blog_id);\n\
        \// define teams and roles\n\
        \right($blog_id, $article_id, $operation) <-\n\
        \  article($blog_id, $article_id),\n\
        \  operation($operation),\n\
        \  user($user_id),\n\
        \  member($user_id, $team_id),\n\
        \  team_role($team_id, $blog_id, \"contributor\"),\n\
        \  [\"read\", \"write\"].contains($operation);\n\
        \ "
      p = Predicate
      sRead = LString "read"
      sWrite = LString "write"
      sContributor = LString "contributor"
      vBlogId = Variable "blog_id"
      vArticleId = Variable "article_id"
      vUserId = Variable "user_id"
      vTeamId = Variable "team_id"
      vOp = Variable "operation"
      bRules =
        [ Rule (p "right" [vBlogId, vArticleId, vOp])
               [ p "article" [vBlogId, vArticleId]
               , p "operation" [vOp]
               , p "user" [vUserId]
               , p "owner" [vUserId, vBlogId]
               ] [] []
        , Rule (p "right" [vBlogId, vArticleId, sRead])
               [ p "article" [vBlogId, vArticleId]
               , p "premium_readable" [vBlogId, vArticleId]
               , p "user" [vUserId]
               , p "premium_user" [vUserId, vBlogId]
               ] [] []
        , Rule (p "right" [vBlogId, vArticleId, vOp])
               [ p "article" [vBlogId, vArticleId]
               , p "operation" [vOp]
               , p "user" [vUserId]
               , p "member" [vUserId, vTeamId]
               , p "team_role" [vTeamId, vBlogId, sContributor]
               ] [EBinary Contains (EValue (TermSet $ Set.fromList [sRead, sWrite]))
                                   (EValue vOp)] []
       ]
      bFacts = []
      bChecks = []
      bContext = Nothing
      bScope = Set.fromList $ BlockId <$> [pk2,pk3,pk5]
  parseBlock spec @?= Right Block{..}