packages feed

biscuit-haskell-0.3.0.1: src/Auth/Biscuit/Datalog/Parser.hs

{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE DerivingStrategies    #-}
{-# LANGUAGE KindSignatures        #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE NamedFieldPuns        #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
{-# LANGUAGE TupleSections         #-}
{-# LANGUAGE TypeApplications      #-}
module Auth.Biscuit.Datalog.Parser
  where

import           Auth.Biscuit.Crypto            (PublicKey,
                                                 readEd25519PublicKey)
import           Auth.Biscuit.Datalog.AST
import           Auth.Biscuit.Utils             (decodeHex)
import           Control.Monad                  (join)
import qualified Control.Monad.Combinators.Expr as Expr
import           Data.Bifunctor
import           Data.ByteString                (ByteString)
import qualified Data.ByteString.Char8          as C8
import           Data.Char
import           Data.Either                    (partitionEithers)
import           Data.Function                  ((&))
import           Data.Int                       (Int64)
import           Data.List.NonEmpty             (NonEmpty)
import qualified Data.List.NonEmpty             as NE
import           Data.Map.Strict                (Map)
import           Data.Maybe                     (isJust)
import           Data.Set                       (Set)
import qualified Data.Set                       as Set
import           Data.Text                      (Text)
import qualified Data.Text                      as T
import           Data.Time                      (UTCTime, defaultTimeLocale,
                                                 parseTimeM)
import           Instances.TH.Lift              ()
import           Language.Haskell.TH
import           Language.Haskell.TH.Quote      (QuasiQuoter (..))
import           Language.Haskell.TH.Syntax     (Lift)
import           Text.Megaparsec
import qualified Text.Megaparsec.Char           as C
import qualified Text.Megaparsec.Char.Lexer     as L
import           Validation                     (Validation (..),
                                                 validationToEither)

type Parser = Parsec SemanticError Text

type Span = (Int, Int)

data SemanticError =
    VarInFact Span
  | VarInSet  Span
  | NestedSet Span
  | InvalidBs Text Span
  | InvalidPublicKey Text Span
  | UnboundVariables (NonEmpty Text) Span
  | PreviousInAuthorizer Span
  deriving stock (Eq, Ord)

instance ShowErrorComponent SemanticError where
  showErrorComponent = \case
    VarInFact _            -> "Variables can't appear in a fact"
    VarInSet  _            -> "Variables can't appear in a set"
    NestedSet _            -> "Sets cannot be nested"
    InvalidBs e _          -> "Invalid bytestring literal: " <> T.unpack e
    InvalidPublicKey e _   -> "Invalid public key: " <> T.unpack e
    UnboundVariables e _   -> "Unbound variables: " <> T.unpack (T.intercalate ", " $ NE.toList e)
    PreviousInAuthorizer _ -> "'previous' can't appear in an authorizer scope"

run :: Parser a -> Text -> Either String a
run p = first errorBundlePretty . runParser (l (pure ()) *> l p <* eof) ""

l :: Parser a -> Parser a
l = L.lexeme $ L.space C.space1 (L.skipLineComment "//") empty

getSpan :: Parser a -> Parser (Span, a)
getSpan p = do
  begin <- getOffset
  a <- p
  end <- getOffset
  pure ((begin, end), a)

registerError :: (Span -> SemanticError) -> Span -> Parser a
registerError mkError sp = do
  let err = FancyError (fst sp) (Set.singleton (ErrorCustom $ mkError sp))
  registerParseError err
  pure $ error "delayed parsing error"

forbid :: (Span -> SemanticError) -> Parser a -> Parser b
forbid mkError p = do
  (sp, _) <- getSpan p
  registerError mkError sp

variableParser :: Parser Text
variableParser =
  C.char '$' *> takeWhile1P (Just "_, :, or any alphanumeric char") (\c -> c == '_' || c == ':' || isAlphaNum c)

haskellVariableParser :: Parser Text
haskellVariableParser = l $ do
  _ <- chunk "{"
  leadingUS <- optional $ C.char '_'
  x <- if isJust leadingUS then C.letterChar else C.lowerChar
  xs <- takeWhileP (Just "_, ', or any alphanumeric char") (\c -> c == '_' || c == '\'' || isAlphaNum c)
  _ <- C.char '}'
  pure . maybe id T.cons leadingUS $ T.cons x xs

setParser :: Parser (Set (Term' 'WithinSet 'InFact 'WithSlices))
setParser = do
  _ <- l $ C.char '['
  ts <- sepBy (termParser (forbid VarInSet variableParser) (forbid NestedSet setParser)) (l $ C.char ',')
  _ <- l $ C.char ']'
  pure $ Set.fromList ts

factTermParser :: Parser (Term' 'NotWithinSet 'InFact 'WithSlices)
factTermParser = termParser (forbid VarInFact variableParser)
                            setParser

predicateTermParser :: Parser (Term' 'NotWithinSet 'InPredicate 'WithSlices)
predicateTermParser = termParser variableParser
                                 setParser

termParser :: Parser (VariableType inSet pof)
           -> Parser (SetType inSet 'WithSlices)
           -> Parser (Term' inSet pof 'WithSlices)
termParser parseVar parseSet = l $ choice
  [ Antiquote . Slice <$> haskellVariableParser <?> "parameter (eg. {paramName})"
  , Variable <$> parseVar <?> "datalog variable (eg. $variable)"
  , TermSet <$> parseSet <?> "set (eg. [1,2,3])"
  , LBytes <$> (chunk "hex:" *> hexParser) <?> "hex-encoded bytestring (eg. hex:00ff99)"
  , LDate <$> rfc3339DateParser <?> "RFC3339-formatted timestamp (eg. 2022-11-29T00:00:00Z)"
  , LInteger <$> intParser <?> "(signed) integer"
  , LString . T.pack <$> (C.char '"' *> manyTill L.charLiteral (C.char '"')) <?> "string literal"
  , LBool <$> choice [ True <$ chunk "true"
                     , False <$ chunk "false"
                     ]
          <?> "boolean value (eg. true or false)"
  ]

intParser :: Parser Int64
intParser = do
  integer :: Integer <- L.signed C.space L.decimal <?> "(signed) integer"
  if integer < fromIntegral (minBound @Int64)
     || integer > fromIntegral (maxBound @Int64)
  then fail "integer literals must fit in the int64 range"
  else pure $ fromIntegral integer

hexParser :: Parser ByteString
hexParser = do
  (sp, hexStr) <- getSpan $ C8.pack <$> some C.hexDigitChar
  case decodeHex hexStr of
    Left e   -> registerError (InvalidBs e) sp
    Right bs -> pure bs

publicKeyParser :: Parser PublicKey
publicKeyParser = do
  (sp, hexStr) <- getSpan $ C8.pack <$> (chunk "ed25519/" *> some C.hexDigitChar)
  case decodeHex hexStr of
    Left e -> registerError (InvalidPublicKey e) sp
    Right bs -> case readEd25519PublicKey bs of
      Nothing -> registerError (InvalidPublicKey "Invalid ed25519 public key") sp
      Just pk -> pure pk

rfc3339DateParser :: Parser UTCTime
rfc3339DateParser = do
  let parseDate = parseTimeM False defaultTimeLocale "%FT%T%Q%EZ"
  input <- sequenceA [
      try (sequenceA [
        C.digitChar,
        C.digitChar,
        C.digitChar,
        C.digitChar,
        C.char '-',
        C.digitChar,
        C.digitChar,
        C.char '-',
        C.digitChar,
        C.digitChar,
        C.char 'T'
      ]),
      pure <$> C.digitChar,
      pure <$> C.digitChar,
      pure <$> C.char ':',
      pure <$> C.digitChar,
      pure <$> C.digitChar,
      pure <$> C.char ':',
      pure <$> C.digitChar,
      pure <$> C.digitChar,
      foldMap join <$> optional (sequenceA [
        pure <$> C.char '.',
        some C.digitChar
      ]),
      choice [
        pure <$> C.char 'Z',
        sequenceA [
           choice [C.char '+', C.char '-'],
           C.digitChar,
           C.digitChar,
           C.char ':',
           C.digitChar,
           C.digitChar
        ]
      ]
    ]
  parseDate $ join input

predicateParser' :: Parser (Term' 'NotWithinSet pof 'WithSlices)
                 -> Parser (Predicate' pof 'WithSlices)
predicateParser' parseTerm = l $ do
  name <- try . (<?> "predicate name") $ do
    x      <- C.letterChar
    xs     <- takeWhileP (Just "_, :, or any alphanumeric char") (\c -> c == '_' || c == ':' || isAlphaNum c)
    _      <- l $ C.char '('
    pure $ T.cons x xs
  terms  <- sepBy1 parseTerm (l $ C.char ',')
  _      <- l $ C.char ')'
  pure Predicate {
    name,
    terms
  }

factParser :: Parser (Predicate' 'InFact 'WithSlices)
factParser = predicateParser' factTermParser

predicateParser :: Parser (Predicate' 'InPredicate 'WithSlices)
predicateParser = predicateParser' predicateTermParser

expressionParser :: Parser (Expression' 'WithSlices)
expressionParser =
  let base = choice [ try methodsParser
                    , exprTerm
                    ]
   in Expr.makeExprParser base table

table :: [[Expr.Operator Parser (Expression' 'WithSlices)]]
table =
  let infixL name op = Expr.InfixL (EBinary op <$ l (chunk name) <?> "infix operator")
      infixN name op = Expr.InfixN (EBinary op <$ l (chunk name) <?> "infix operator")
      prefix name op = Expr.Prefix (EUnary op <$  l (chunk name) <?> "prefix operator")
   in [ [ prefix "!" Negate]
      , [ infixL  "*" Mul
        , infixL  "/" Div
        ]
      , [ infixL  "+" Add
        , infixL  "-" Sub
        ]
      -- TODO find a better way to avoid eager parsing
      -- of && and || by the bitwise operators
      , [ infixL  "& " BitwiseAnd ]
      , [ infixL  "| " BitwiseOr  ]
      , [ infixL  "^" BitwiseXor ]
      , [ infixN  "<=" LessOrEqual
        , infixN  ">=" GreaterOrEqual
        , infixN  "<"  LessThan
        , infixN  ">"  GreaterThan
        , infixN  "==" Equal
        , infixN  "!=" NotEqual
        ]
      , [ infixL "&&" And ]
      , [ infixL "||" Or ]
      ]

binaryMethodParser :: Parser (Expression' 'WithSlices -> Expression' 'WithSlices)
binaryMethodParser = do
  _ <- C.char '.'
  method <- choice
    [ Contains     <$ chunk "contains"
    , Intersection <$ chunk "intersection"
    , Union        <$ chunk "union"
    , Prefix       <$ chunk "starts_with"
    , Suffix       <$ chunk "ends_with"
    , Regex        <$ chunk "matches"
    ]
  _ <- l $ C.char '('
  e2 <- l expressionParser
  _ <- l $ C.char ')'
  pure $ \e1 -> EBinary method e1 e2

unaryMethodParser :: Parser (Expression' 'WithSlices -> Expression' 'WithSlices)
unaryMethodParser = do
  _ <- C.char '.'
  method <- Length <$ chunk "length"
  _ <- l $ chunk "()"
  pure $ EUnary method

methodsParser :: Parser (Expression' 'WithSlices)
methodsParser = do
  e1 <- exprTerm
  methods <- some (try binaryMethodParser <|> unaryMethodParser)
  pure $ foldl (&) e1 methods

unaryParens :: Parser (Expression' 'WithSlices)
unaryParens = do
  _ <- l $ C.char '('
  e <- l expressionParser
  _ <- l $ C.char ')'
  pure $ EUnary Parens e

exprTerm :: Parser (Expression' 'WithSlices)
exprTerm = choice
  [ unaryParens <?> "parens"
  , EValue <$> predicateTermParser
  ]

ruleParser :: Bool -> Parser (Rule' 'Repr 'WithSlices)
ruleParser inAuthorizer = do
  begin <- getOffset
  rhead <- try $ l predicateParser <* l (chunk "<-")
  (body, expressions, scope) <- ruleBodyParser inAuthorizer
  end <- getOffset
  case makeRule rhead body expressions scope of
    Failure vs -> registerError (UnboundVariables vs) (begin, end)
    Success r  -> pure r

ruleBodyParser :: Bool -> Parser ([Predicate' 'InPredicate 'WithSlices], [Expression' 'WithSlices], Set.Set (RuleScope' 'Repr 'WithSlices))
ruleBodyParser inAuthorizer = do
  let predicateOrExprParser =
            Left  <$> (predicateParser <?> "predicate")
        <|> Right <$> (expressionParser <?> "expression")
  elems <- l $ sepBy1 (l predicateOrExprParser)
                      (l $ C.char ',')
  scope <- option Set.empty $ scopeParser inAuthorizer
  let (predicates, expressions) = partitionEithers elems
  pure (predicates, expressions, scope)

scopeParser :: Bool -> Parser (Set.Set (RuleScope' 'Repr 'WithSlices))
scopeParser inAuthorizer = (<?> "scope annotation") $ do
  _ <- l $ chunk "trusting "
  let elemParser = do
        (sp, s) <- getSpan $ choice [ OnlyAuthority <$  chunk "authority"
                                    , Previous      <$  chunk "previous"
                                    , BlockId       <$>
                                       choice [ PkSlice <$> haskellVariableParser <?> "parameter (eg. {paramName})"
                                              , Pk <$> publicKeyParser <?> "public key (eg. ed25519/00ff99)"
                                              ]
                                    ]
        if inAuthorizer && s == Previous
        then registerError PreviousInAuthorizer sp
        else pure s
  Set.fromList <$> sepBy1 (l elemParser)
                          (l $ C.char ',')

queryItemParser :: Bool -> Parser (QueryItem' 'Repr 'WithSlices)
queryItemParser inAuthorizer = do
  (sp, (predicates, expressions, scope)) <- getSpan $ ruleBodyParser inAuthorizer
  case makeQueryItem predicates expressions scope of
    Failure e  -> registerError (UnboundVariables e) sp
    Success qi -> pure qi

queryParser :: Bool -> Parser [QueryItem' 'Repr 'WithSlices]
queryParser inAuthorizer =
   sepBy1 (queryItemParser inAuthorizer) (l $ C.string' "or" <* C.space)
     <?> "datalog query"

checkParser :: Bool -> Parser (Check' 'Repr 'WithSlices)
checkParser inAuthorizer = do
  cKind <- l $ choice [ One <$ chunk "check if"
                      , All <$ chunk "check all"
                      ]
  cQueries <- queryParser inAuthorizer
  pure Check{..}

policyParser :: Parser (Policy' 'Repr 'WithSlices)
policyParser = do
  policy <- l $ choice [ Allow <$ chunk "allow if"
                       , Deny  <$ chunk "deny if"
                       ]
  (policy, ) <$> queryParser True

blockElementParser :: Bool -> Parser (BlockElement' 'Repr 'WithSlices)
blockElementParser inAuthorizer = choice
  [ BlockCheck   <$> checkParser inAuthorizer <* C.char ';' <?> "check"
  , BlockRule    <$> ruleParser  inAuthorizer <* C.char ';' <?> "rule"
  , BlockFact    <$> factParser <* C.char ';' <?> "fact"
  ]

authorizerElementParser :: Parser (AuthorizerElement' 'Repr 'WithSlices)
authorizerElementParser = choice
  [ AuthorizerPolicy  <$> policyParser <* C.char ';' <?> "policy"
  , BlockElement    <$> blockElementParser True
  ]

blockParser :: Parser (Block' 'Repr 'WithSlices)
blockParser = do
  bScope <- option Set.empty $ l (scopeParser False <* C.char ';' <?> "scope annotation")
  elems <- many $ l $ blockElementParser False
  pure $ (foldMap elementToBlock elems) { bScope = bScope }

authorizerParser :: Parser (Authorizer' 'Repr 'WithSlices)
authorizerParser = do
  bScope <- option Set.empty $ l (scopeParser True <* C.char ';' <?> "scope annotation")
  elems <- many $ l authorizerElementParser
  let addScope a = a { vBlock = (vBlock a) { bScope = bScope } }
  pure $ addScope $ foldMap elementToAuthorizer elems

parseWithParams :: Parser (a 'WithSlices)
                -> (Map Text Value -> Map Text PublicKey -> a 'WithSlices -> Validation (NonEmpty Text) (a 'Representation))
                -> Text
                -> Map Text Value -> Map Text PublicKey
                -> Either (NonEmpty Text) (a 'Representation)
parseWithParams parser substitute input termMapping keyMapping = do
  withSlices <- first (pure . T.pack) $ run parser input
  validationToEither $ substitute termMapping keyMapping withSlices

parseBlock :: Text -> Map Text Value -> Map Text PublicKey
           -> Either (NonEmpty Text) Block
parseBlock = parseWithParams blockParser substituteBlock

parseAuthorizer :: Text -> Map Text Value -> Map Text PublicKey
                -> Either (NonEmpty Text) Authorizer
parseAuthorizer = parseWithParams authorizerParser substituteAuthorizer

compileParser :: Lift a => Parser a -> (a -> Q Exp) -> String -> Q Exp
compileParser p build =
  either fail build . run p . T.pack

-- | Quasiquoter for a rule expression. You can reference haskell variables
-- like this: @{variableName}@.
--
-- You most likely want to directly use 'block' or 'authorizer' instead.
rule :: QuasiQuoter
rule = QuasiQuoter
  { quoteExp = compileParser (ruleParser False) $ \result -> [| result :: Rule |]
  , quotePat = error "not supported"
  , quoteType = error "not supported"
  , quoteDec = error "not supported"
  }

-- | Quasiquoter for a predicate expression. You can reference haskell variables
-- like this: @{variableName}@.
--
-- You most likely want to directly use 'block' or 'authorizer' instead.
predicate :: QuasiQuoter
predicate = QuasiQuoter
  { quoteExp = compileParser predicateParser $ \result -> [| result :: Predicate |]
  , quotePat = error "not supported"
  , quoteType = error "not supported"
  , quoteDec = error "not supported"
  }

-- | Quasiquoter for a fact expression. You can reference haskell variables
-- like this: @{variableName}@.
--
-- You most likely want to directly use 'block' or 'authorizer' instead.
fact :: QuasiQuoter
fact = QuasiQuoter
  { quoteExp = compileParser factParser $ \result -> [| result :: Fact |]
  , quotePat = error "not supported"
  , quoteType = error "not supported"
  , quoteDec = error "not supported"
  }

-- | Quasiquoter for a check expression. You can reference haskell variables
-- like this: @{variableName}@.
--
-- You most likely want to directly use 'block' or 'authorizer' instead.
check :: QuasiQuoter
check = QuasiQuoter
  { quoteExp = compileParser (checkParser False) $ \result -> [| result :: Check |]
  , quotePat = error "not supported"
  , quoteType = error "not supported"
  , quoteDec = error "not supported"
  }

-- | Compile-time parser for a block expression, intended to be used with the
-- @QuasiQuotes@ extension.
--
-- A typical use of 'block' looks like this:
--
-- > let fileName = "data.pdf"
-- >  in [block|
-- >       // datalog can reference haskell variables with {variableName}
-- >       resource({fileName});
-- >       rule($variable) <- fact($value), other_fact($value);
-- >       check if operation("read");
-- >     |]
block :: QuasiQuoter
block = QuasiQuoter
  { quoteExp = compileParser blockParser $ \result -> [| result :: Block |]
  , quotePat = error "not supported"
  , quoteType = error "not supported"
  , quoteDec = error "not supported"
  }

-- | Compile-time parser for an authorizer expression, intended to be used with the
-- @QuasiQuotes@ extension.
--
-- A typical use of 'authorizer' looks like this:
--
-- > do
-- >   now <- getCurrentTime
-- >   pure [authorizer|
-- >          // datalog can reference haskell variables with {variableName}
-- >          current_time({now});
-- >          // authorizers can contain facts, rules and checks like blocks, but
-- >          // also declare policies. While every check has to pass for a biscuit to
-- >          // be valid, policies are tried in order. The first one to match decides
-- >          // if the token is valid or not
-- >          allow if resource("file1");
-- >          deny if true;
-- >        |]
authorizer :: QuasiQuoter
authorizer = QuasiQuoter
  { quoteExp = compileParser authorizerParser $ \result -> [| result :: Authorizer |]
  , quotePat = error "not supported"
  , quoteType = error "not supported"
  , quoteDec = error "not supported"
  }

-- | Compile-time parser for a query expression, intended to be used with the
-- @QuasiQuotes@ extension.
--
-- A typical use of 'query' looks like this:
--
-- > [query|user($user_id) or group($group_id)|]
query :: QuasiQuoter
query = QuasiQuoter
  { quoteExp = compileParser (queryParser False) $ \result -> [| result :: Query |]
  , quotePat = error "not supported"
  , quoteType = error "not supported"
  , quoteDec = error "not supported"
  }