biscuit-haskell-0.2.0.0: src/Auth/Biscuit/Datalog/ScopedExecutor.hs
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
module Auth.Biscuit.Datalog.ScopedExecutor
( BlockWithRevocationId
, runAuthorizer
, runAuthorizerWithLimits
, runAuthorizerNoTimeout
, World (..)
, computeAllFacts
, runFactGeneration
, PureExecError (..)
, AuthorizationSuccess (..)
, getBindings
, queryAuthorizerFacts
, getVariableValues
, getSingleVariableValue
) where
import Control.Monad (join, when)
import Control.Monad.State (StateT (..), get, lift, modify,
put, runStateT)
import Data.Bifunctor (first)
import Data.ByteString (ByteString)
import Data.Foldable (traverse_)
import Data.List.NonEmpty (NonEmpty, nonEmpty)
import qualified Data.List.NonEmpty as NE
import Data.Map.Strict ((!?))
import Data.Maybe (mapMaybe)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text, intercalate, unpack)
import Validation (Validation (..), validation)
import Auth.Biscuit.Datalog.AST
import Auth.Biscuit.Datalog.Executor (Bindings, ExecutionError (..),
Limits (..), MatchedQuery (..),
ResultError (..), checkCheck,
checkPolicy, defaultLimits,
getBindingsForRuleBody,
getFactsForRule)
import Auth.Biscuit.Datalog.Parser (fact)
import Auth.Biscuit.Timer (timer)
type BlockWithRevocationId = (Block, ByteString)
-- | A subset of 'ExecutionError' that can only happen during fact generation
data PureExecError = Facts | Iterations
deriving (Eq, Show)
-- | State maintained by the datalog computation.
data ComputeState
= ComputeState
{ sFacts :: Set Fact
-- ^ All the facts generated so far
, sAuthorityFacts :: Set Fact
-- ^ Facts generated by the authority block (and the authorizer). Those are kept separate
-- because they are provided by a trusted party (the one which has the root 'SecretKey').
-- Block facts are not as trustworthy as they can be added by anyone.
, sIterations :: Int
-- ^ The current count of iterations
, sLimits :: Limits
-- ^ The configured limits for this computation. This field is effectively read-only
, sFailedChecks :: [Check]
-- ^ The failed checks gathered so far. The computation carries on even if some checks
-- fail, in order to be able to report all the failing checks in one go
, sPolicyResult :: Either (Maybe MatchedQuery) MatchedQuery
-- ^ The result of the authorizer-defined policies. 'Left' represents failure:
-- - @Left Nothing@ if no policies matched
-- - @Left (Just q)@ if a deny policy matched
-- - @Right q@ if an allow policy matched
}
mkInitState :: Limits -> ComputeState
mkInitState sLimits = ComputeState
{ sFacts = Set.empty -- no facts have been generated yet
, sAuthorityFacts = Set.empty -- no authority facts have been generated yet
, sIterations = 0 -- no evaluation iteration has taken place yet
, sLimits -- this field is read-only
, sFailedChecks = [] -- no checks have failed yet
, sPolicyResult = Left Nothing -- no policies have matched yet
}
data World
= World
{ facts :: Set Fact
, rules :: Set Rule
}
instance Semigroup World where
w1 <> w2 = World
{ rules = rules w1 <> rules w2
, facts = facts w1 <> facts w2
}
instance Monoid World where
mempty = World mempty mempty
instance Show World where
show World{..} = unpack . intercalate "\n" $ join
[ [ "Block Rules" ]
, renderRule <$> Set.toList rules
, [ "Facts" ]
, renderFact <$> Set.toList facts
]
-- | Proof that a biscuit was authorized successfully. In addition to the matched
-- @allow query@, the generated facts are kept around for further querying.
-- Since only authority facts can be trusted, they are kept separate.
data AuthorizationSuccess
= AuthorizationSuccess
{ matchedAllowQuery :: MatchedQuery
-- ^ The allow query that matched
, authorityFacts :: Set Fact
-- ^ All the facts generated by the authority block (and the authorizer)
, allGeneratedFacts :: Set Fact
-- ^ All the facts that were generated by the biscuit. Be careful, the
-- biscuit signature check only guarantees that 'authorityFacts' are
-- signed with the corresponding 'SecretKey'.
, limits :: Limits
-- ^ Limits used when running datalog. It is kept around to allow further
-- datalog computation when querying facts
}
deriving (Eq, Show)
-- | Get the matched variables from the @allow@ query used to authorize the biscuit.
-- This can be used in conjuction with 'getVariableValues' or 'getSingleVariableValue'
-- to extract the actual values
getBindings :: AuthorizationSuccess -> Set Bindings
getBindings AuthorizationSuccess{matchedAllowQuery=MatchedQuery{bindings}} = bindings
withFacts :: World -> Set Fact -> World
withFacts w@World{facts} newFacts = w { facts = newFacts <> facts }
-- | Given a series of blocks and an authorizer, ensure that all
-- the checks and policies match
runAuthorizer :: BlockWithRevocationId
-- ^ The authority block
-> [BlockWithRevocationId]
-- ^ The extra blocks
-> Authorizer
-- ^ A authorizer
-> IO (Either ExecutionError AuthorizationSuccess)
runAuthorizer = runAuthorizerWithLimits defaultLimits
-- | Given a series of blocks and an authorizer, ensure that all
-- the checks and policies match, with provided execution
-- constraints
runAuthorizerWithLimits :: Limits
-- ^ custom limits
-> BlockWithRevocationId
-- ^ The authority block
-> [BlockWithRevocationId]
-- ^ The extra blocks
-> Authorizer
-- ^ A authorizer
-> IO (Either ExecutionError AuthorizationSuccess)
runAuthorizerWithLimits l@Limits{..} authority blocks v = do
resultOrTimeout <- timer maxTime $ pure $ runAuthorizerNoTimeout l authority blocks v
pure $ case resultOrTimeout of
Nothing -> Left Timeout
Just r -> r
runAllBlocks :: BlockWithRevocationId
-> [BlockWithRevocationId]
-> Authorizer
-> StateT ComputeState (Either PureExecError) ()
runAllBlocks authority blocks authorizer = do
modify $ \state -> state { sFacts = mkRevocationIdFacts authority blocks }
runAuthority authority authorizer
traverse_ runBlock blocks
mkRevocationIdFacts :: BlockWithRevocationId -> [BlockWithRevocationId]
-> Set Fact
mkRevocationIdFacts authority blocks =
let allIds :: [(Int, ByteString)]
allIds = zip [0..] $ snd <$> authority : blocks
mkFact (index, rid) = [fact|revocation_id(${index}, ${rid})|]
in Set.fromList $ mkFact <$> allIds
runAuthorizerNoTimeout :: Limits
-> BlockWithRevocationId
-> [BlockWithRevocationId]
-> Authorizer
-> Either ExecutionError AuthorizationSuccess
runAuthorizerNoTimeout limits authority blocks authorizer = do
let result = (`runStateT` mkInitState limits) $ runAllBlocks authority blocks authorizer
case result of
Left Facts -> Left TooManyFacts
Left Iterations -> Left TooManyIterations
Right ((), ComputeState{..}) -> case (nonEmpty sFailedChecks, sPolicyResult) of
(Nothing, Right p) -> Right $ AuthorizationSuccess { matchedAllowQuery = p
, authorityFacts = sAuthorityFacts
, allGeneratedFacts = sFacts
, limits
}
(Nothing, Left Nothing) -> Left $ ResultError $ NoPoliciesMatched []
(Nothing, Left (Just p)) -> Left $ ResultError $ DenyRuleMatched [] p
(Just cs, Left Nothing) -> Left $ ResultError $ NoPoliciesMatched (NE.toList cs)
(Just cs, Left (Just p)) -> Left $ ResultError $ DenyRuleMatched (NE.toList cs) p
(Just cs, Right _) -> Left $ ResultError $ FailedChecks cs
runFactGeneration :: Limits -> World -> Either PureExecError (Set Fact)
runFactGeneration limits w =
let getFacts = sFacts . snd
in getFacts <$> runStateT (computeAllFacts w) (mkInitState limits)
runAuthority :: BlockWithRevocationId
-> Authorizer
-> StateT ComputeState (Either PureExecError) ()
runAuthority (block, _rid) Authorizer{..} = do
let world = collectWorld block <> collectWorld vBlock
computeAllFacts world
-- store the facts generated by the authority block (and the authorizer)
-- in a dedicated `sAuthorityFacts` so that they can be queried independently
-- later: we trust the authority facts, not the block facts
modify $ \c@ComputeState{sFacts} -> c { sAuthorityFacts = sFacts }
state@ComputeState{sFacts, sLimits} <- get
let checkResults = checkChecks sLimits (bChecks block <> bChecks vBlock) sFacts
let policyResult = checkPolicies sLimits vPolicies sFacts
put state { sPolicyResult = policyResult
, sFailedChecks = validation NE.toList mempty checkResults
}
runBlock :: BlockWithRevocationId
-> StateT ComputeState (Either PureExecError) ()
runBlock (block@Block{bChecks}, _rid) = do
let world = collectWorld block
computeAllFacts world
state@ComputeState{sFacts, sLimits, sFailedChecks} <- get
let checkResults = checkChecks sLimits bChecks sFacts
put state { sFailedChecks = validation NE.toList mempty checkResults <> sFailedChecks
}
checkChecks :: Limits -> [Check] -> Set Fact -> Validation (NonEmpty Check) ()
checkChecks limits checks facts = traverse_ (checkCheck limits facts) checks
checkPolicies :: Limits -> [Policy] -> Set Fact -> Either (Maybe MatchedQuery) MatchedQuery
checkPolicies limits policies facts =
let results = mapMaybe (checkPolicy limits facts) policies
in case results of
p : _ -> first Just p
[] -> Left Nothing
computeAllFacts :: World
-> StateT ComputeState (Either PureExecError) ()
computeAllFacts world = do
state@ComputeState{..} <- get
let Limits{..} = sLimits
let newFacts = extend sLimits (world `withFacts` sFacts)
allFacts = sFacts <> facts world <> newFacts
when (Set.size allFacts >= maxFacts) $ lift $ Left Facts
when (sIterations >= maxIterations) $ lift $ Left Iterations
put $ state { sIterations = sIterations + 1
, sFacts = allFacts
}
if null newFacts
then pure ()
else computeAllFacts world
extend :: Limits -> World -> Set Fact
extend l World{..} =
let buildFacts = foldMap (getFactsForRule l facts)
allNewFacts = buildFacts rules
in Set.difference allNewFacts facts
collectWorld :: Block -> World
collectWorld Block{..} = World
{ facts = Set.fromList bFacts
, rules = Set.fromList bRules
}
-- | Query the facts generated by the authority and authorizer blocks
-- during authorization. This can be used in conjuction with 'getVariableValues'
-- and 'getSingleVariableValue' to retrieve actual values.
--
-- ⚠ Only the facts generated by the authority and authorizer blocks are queried.
-- Block facts are not queried (since they can't be trusted).
--
-- 💁 If the facts you want to query are part of an allow query in the authorizer,
-- you can directly get values from 'AuthorizationSuccess'.
queryAuthorizerFacts :: AuthorizationSuccess -> Query -> Set Bindings
queryAuthorizerFacts AuthorizationSuccess{authorityFacts, limits} q =
let getBindingsForQueryItem QueryItem{qBody,qExpressions} =
getBindingsForRuleBody limits authorityFacts qBody qExpressions
in foldMap getBindingsForQueryItem q
-- | Extract a set of values from a matched variable for a specific type.
-- Returning @Set Value@ allows to get all values, whatever their type.
getVariableValues :: (Ord t, FromValue t)
=> Set Bindings
-> Text
-> Set t
getVariableValues bindings variableName =
let mapMaybeS f = foldMap (foldMap Set.singleton . f)
getVar vars = fromValue =<< vars !? variableName
in mapMaybeS getVar bindings
-- | Extract exactly one value from a matched variable. If the variable has 0
-- matches or more than one match, 'Nothing' will be returned
getSingleVariableValue :: (Ord t, FromValue t)
=> Set Bindings
-> Text
-> Maybe t
getSingleVariableValue bindings variableName =
let values = getVariableValues bindings variableName
in case Set.toList values of
[v] -> Just v
_ -> Nothing