biscuit-haskell-0.3.0.0: src/Auth/Biscuit/ProtoBufAdapter.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
{-|
Module : Auth.Biscuit.Utils
Copyright : © Clément Delafargue, 2021
License : MIT
Maintainer : clement@delafargue.name
Conversion functions between biscuit components and protobuf-encoded components
-}
module Auth.Biscuit.ProtoBufAdapter
( Symbols
, buildSymbolTable
, pbToBlock
, blockToPb
, pbToSignedBlock
, signedBlockToPb
, pbToProof
, pbToThirdPartyBlockRequest
, thirdPartyBlockRequestToPb
, pbToThirdPartyBlockContents
, thirdPartyBlockContentsToPb
) where
import Control.Monad (when)
import Control.Monad.State (StateT, get, lift, modify)
import Data.Bitraversable (bisequence)
import Data.ByteString (ByteString)
import Data.Int (Int64)
import qualified Data.List.NonEmpty as NE
import Data.Maybe (isNothing)
import qualified Data.Set as Set
import qualified Data.Text as T
import Data.Time (UTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime,
utcTimeToPOSIXSeconds)
import Data.Void (absurd)
import GHC.Records (getField)
import Validation (Validation (..))
import qualified Auth.Biscuit.Crypto as Crypto
import Auth.Biscuit.Datalog.AST
import qualified Auth.Biscuit.Proto as PB
import Auth.Biscuit.Symbols
import Auth.Biscuit.Utils (maybeToRight)
buildSymbolTable :: Symbols -> Block -> BlockSymbols
buildSymbolTable existingSymbols block =
let allSymbols = listSymbolsInBlock block
allKeys = listPublicKeysInBlock block
in addSymbols existingSymbols allSymbols allKeys
pbToPublicKey :: PB.PublicKey -> Either String Crypto.PublicKey
pbToPublicKey PB.PublicKey{..} =
let keyBytes = PB.getField key
parseKey = Crypto.readEd25519PublicKey
in case PB.getField algorithm of
PB.Ed25519 -> maybeToRight "Invalid ed25519 public key" $ parseKey keyBytes
pbToOptionalSignature :: PB.ExternalSig -> Either String (Crypto.Signature, Crypto.PublicKey)
pbToOptionalSignature PB.ExternalSig{..} = do
let sig = Crypto.signature $ PB.getField signature
pk <- pbToPublicKey $ PB.getField publicKey
pure (sig, pk)
-- | Parse a protobuf signed block into a signed biscuit block
pbToSignedBlock :: PB.SignedBlock -> Either String Crypto.SignedBlock
pbToSignedBlock PB.SignedBlock{..} = do
let sig = Crypto.signature $ PB.getField signature
mSig <- traverse pbToOptionalSignature $ PB.getField externalSig
pk <- pbToPublicKey $ PB.getField nextKey
pure ( PB.getField block
, sig
, pk
, mSig
)
publicKeyToPb :: Crypto.PublicKey -> PB.PublicKey
publicKeyToPb pk = PB.PublicKey
{ algorithm = PB.putField PB.Ed25519
, key = PB.putField $ Crypto.pkBytes pk
}
externalSigToPb :: (Crypto.Signature, Crypto.PublicKey) -> PB.ExternalSig
externalSigToPb (sig, pk) = PB.ExternalSig
{ signature = PB.putField $ Crypto.sigBytes sig
, publicKey = PB.putField $ publicKeyToPb pk
}
signedBlockToPb :: Crypto.SignedBlock -> PB.SignedBlock
signedBlockToPb (block, sig, pk, eSig) = PB.SignedBlock
{ block = PB.putField block
, signature = PB.putField $ Crypto.sigBytes sig
, nextKey = PB.putField $ publicKeyToPb pk
, externalSig = PB.putField $ externalSigToPb <$> eSig
}
pbToProof :: PB.Proof -> Either String (Either Crypto.Signature Crypto.SecretKey)
pbToProof (PB.ProofSignature rawSig) = Left <$> Right (Crypto.signature $ PB.getField rawSig)
pbToProof (PB.ProofSecret rawPk) = Right <$> maybeToRight "Invalid public key proof" (Crypto.readEd25519SecretKey $ PB.getField rawPk)
pbToBlock :: Maybe Crypto.PublicKey -> PB.Block -> StateT Symbols (Either String) Block
pbToBlock ePk PB.Block{..} = do
blockPks <- lift $ traverse pbToPublicKey $ PB.getField pksTable
let blockSymbols = PB.getField symbols
-- third party blocks use an isolated symbol table,
-- but use the global public keys table:
-- symbols defined in 3rd party blocks are not visible
-- to following blocks, but public keys are
when (isNothing ePk) $ modify (registerNewSymbols blockSymbols)
modify (registerNewPublicKeys $ foldMap pure ePk <> blockPks)
currentSymbols <- get
let symbolsForCurrentBlock =
-- third party blocks use an isolated symbol table,
-- but use the global public keys table.
-- 3rd party blocks don't see previously defined
-- symbols, but see previously defined public keys
if isNothing ePk then currentSymbols
else registerNewSymbols blockSymbols $ forgetSymbols currentSymbols
let bContext = PB.getField context
bVersion = PB.getField version
lift $ do
let s = symbolsForCurrentBlock
bFacts <- traverse (pbToFact s) $ PB.getField facts_v2
bRules <- traverse (pbToRule s) $ PB.getField rules_v2
bChecks <- traverse (pbToCheck s) $ PB.getField checks_v2
bScope <- Set.fromList <$> traverse (pbToScope s) (PB.getField scope)
let isV3 = isNothing ePk
&& Set.null bScope
&& all ruleHasNoScope bRules
&& all queryHasNoScope (cQueries <$> bChecks)
&& all isCheckOne bChecks
&& all ruleHasNoV4Operators bRules
&& all queryHasNoV4Operators (cQueries <$> bChecks)
case (bVersion, isV3) of
(Just 4, _) -> pure Block {..}
(Just 3, True) -> pure Block {..}
(Just 3, False) ->
Left "Biscuit v4 fields are present, but the block version is 3."
_ ->
Left $ "Unsupported biscuit version: " <> maybe "0" show bVersion <> ". Only versions 3 and 4 are supported"
-- | Turn a biscuit block into a protobuf block, for serialization,
-- along with the newly defined symbols
blockToPb :: Bool -> Symbols -> Block -> (BlockSymbols, PB.Block)
blockToPb hasExternalPk existingSymbols b@Block{..} =
let isV3 = not hasExternalPk
&& Set.null bScope
&& all ruleHasNoScope bRules
&& all queryHasNoScope (cQueries <$> bChecks)
&& all isCheckOne bChecks
&& all ruleHasNoV4Operators bRules
&& all queryHasNoV4Operators (cQueries <$> bChecks)
bSymbols = buildSymbolTable existingSymbols b
s = reverseSymbols $ addFromBlock existingSymbols bSymbols
symbols = PB.putField $ getSymbolList bSymbols
context = PB.putField bContext
facts_v2 = PB.putField $ factToPb s <$> bFacts
rules_v2 = PB.putField $ ruleToPb s <$> bRules
checks_v2 = PB.putField $ checkToPb s <$> bChecks
scope = PB.putField $ scopeToPb s <$> Set.toList bScope
pksTable = PB.putField $ publicKeyToPb <$> getPkList bSymbols
version = PB.putField $ if isV3 then Just 3
else Just 4
in (bSymbols, PB.Block {..})
pbToFact :: Symbols -> PB.FactV2 -> Either String Fact
pbToFact s PB.FactV2{predicate} = do
let pbName = PB.getField $ PB.name $ PB.getField predicate
pbTerms = PB.getField $ PB.terms $ PB.getField predicate
name <- getSymbol s $ SymbolRef pbName
terms <- traverse (pbToValue s) pbTerms
pure Predicate{..}
factToPb :: ReverseSymbols -> Fact -> PB.FactV2
factToPb s Predicate{..} =
let
predicate = PB.PredicateV2
{ name = PB.putField $ getSymbolRef $ getSymbolCode s name
, terms = PB.putField $ valueToPb s <$> terms
}
in PB.FactV2{predicate = PB.putField predicate}
pbToRule :: Symbols -> PB.RuleV2 -> Either String Rule
pbToRule s pbRule = do
let pbHead = PB.getField $ PB.head pbRule
pbBody = PB.getField $ PB.body pbRule
pbExpressions = PB.getField $ PB.expressions pbRule
pbScope = PB.getField $ getField @"scope" pbRule
rhead <- pbToPredicate s pbHead
body <- traverse (pbToPredicate s) pbBody
expressions <- traverse (pbToExpression s) pbExpressions
scope <- Set.fromList <$> traverse (pbToScope s) pbScope
case makeRule rhead body expressions scope of
Failure vs -> Left $ "Unbound variables in rule: " <> T.unpack (T.intercalate ", " $ NE.toList vs)
Success r -> pure r
ruleToPb :: ReverseSymbols -> Rule -> PB.RuleV2
ruleToPb s Rule{..} =
PB.RuleV2
{ head = PB.putField $ predicateToPb s rhead
, body = PB.putField $ predicateToPb s <$> body
, expressions = PB.putField $ expressionToPb s <$> expressions
, scope = PB.putField $ scopeToPb s <$> Set.toList scope
}
pbToCheck :: Symbols -> PB.CheckV2 -> Either String Check
pbToCheck s PB.CheckV2{queries,kind} = do
let toCheck Rule{body,expressions,scope} = QueryItem{qBody = body, qExpressions = expressions, qScope = scope}
rules <- traverse (pbToRule s) $ PB.getField queries
let cQueries = toCheck <$> rules
let cKind = case PB.getField kind of
Just PB.All -> All
Just PB.One -> One
Nothing -> One
pure Check{..}
checkToPb :: ReverseSymbols -> Check -> PB.CheckV2
checkToPb s Check{..} =
let dummyHead = Predicate "query" []
toQuery QueryItem{..} =
ruleToPb s $ Rule { rhead = dummyHead
, body = qBody
, expressions = qExpressions
, scope = qScope
}
pbKind = case cKind of
One -> Nothing
All -> Just PB.All
in PB.CheckV2 { queries = PB.putField $ toQuery <$> cQueries
, kind = PB.putField pbKind
}
pbToScope :: Symbols -> PB.Scope -> Either String RuleScope
pbToScope s = \case
PB.ScType e -> case PB.getField e of
PB.ScopeAuthority -> Right OnlyAuthority
PB.ScopePrevious -> Right Previous
PB.ScBlock pkRef ->
BlockId <$> getPublicKey' s (PublicKeyRef $ PB.getField pkRef)
scopeToPb :: ReverseSymbols -> RuleScope -> PB.Scope
scopeToPb s = \case
OnlyAuthority -> PB.ScType $ PB.putField PB.ScopeAuthority
Previous -> PB.ScType $ PB.putField PB.ScopePrevious
BlockId pk -> PB.ScBlock $ PB.putField $ getPublicKeyCode s pk
pbToPredicate :: Symbols -> PB.PredicateV2 -> Either String (Predicate' 'InPredicate 'Representation)
pbToPredicate s pbPredicate = do
let pbName = PB.getField $ PB.name pbPredicate
pbTerms = PB.getField $ PB.terms pbPredicate
name <- getSymbol s $ SymbolRef pbName
terms <- traverse (pbToTerm s) pbTerms
pure Predicate{..}
predicateToPb :: ReverseSymbols -> Predicate -> PB.PredicateV2
predicateToPb s Predicate{..} =
PB.PredicateV2
{ name = PB.putField $ getSymbolRef $ getSymbolCode s name
, terms = PB.putField $ termToPb s <$> terms
}
pbTimeToUtcTime :: Int64 -> UTCTime
pbTimeToUtcTime = posixSecondsToUTCTime . fromIntegral
pbToTerm :: Symbols -> PB.TermV2 -> Either String Term
pbToTerm s = \case
PB.TermInteger f -> pure $ LInteger $ fromIntegral $ PB.getField f
PB.TermString f -> LString <$> getSymbol s (SymbolRef $ PB.getField f)
PB.TermDate f -> pure $ LDate $ pbTimeToUtcTime $ PB.getField f
PB.TermBytes f -> pure $ LBytes $ PB.getField f
PB.TermBool f -> pure $ LBool $ PB.getField f
PB.TermVariable f -> Variable <$> getSymbol s (SymbolRef $ PB.getField f)
PB.TermTermSet f -> TermSet . Set.fromList <$> traverse (pbToSetValue s) (PB.getField . PB.set $ PB.getField f)
termToPb :: ReverseSymbols -> Term -> PB.TermV2
termToPb s = \case
Variable n -> PB.TermVariable $ PB.putField $ getSymbolRef $ getSymbolCode s n
LInteger v -> PB.TermInteger $ PB.putField $ fromIntegral v
LString v -> PB.TermString $ PB.putField $ getSymbolRef $ getSymbolCode s v
LDate v -> PB.TermDate $ PB.putField $ round $ utcTimeToPOSIXSeconds v
LBytes v -> PB.TermBytes $ PB.putField v
LBool v -> PB.TermBool $ PB.putField v
TermSet vs -> PB.TermTermSet $ PB.putField $ PB.TermSet $ PB.putField $ setValueToPb s <$> Set.toList vs
Antiquote v -> absurd v
pbToValue :: Symbols -> PB.TermV2 -> Either String Value
pbToValue s = \case
PB.TermInteger f -> pure $ LInteger $ fromIntegral $ PB.getField f
PB.TermString f -> LString <$> getSymbol s (SymbolRef $ PB.getField f)
PB.TermDate f -> pure $ LDate $ pbTimeToUtcTime $ PB.getField f
PB.TermBytes f -> pure $ LBytes $ PB.getField f
PB.TermBool f -> pure $ LBool $ PB.getField f
PB.TermVariable _ -> Left "Variables can't appear in facts"
PB.TermTermSet f -> TermSet . Set.fromList <$> traverse (pbToSetValue s) (PB.getField . PB.set $ PB.getField f)
valueToPb :: ReverseSymbols -> Value -> PB.TermV2
valueToPb s = \case
LInteger v -> PB.TermInteger $ PB.putField $ fromIntegral v
LString v -> PB.TermString $ PB.putField $ getSymbolRef $ getSymbolCode s v
LDate v -> PB.TermDate $ PB.putField $ round $ utcTimeToPOSIXSeconds v
LBytes v -> PB.TermBytes $ PB.putField v
LBool v -> PB.TermBool $ PB.putField v
TermSet vs -> PB.TermTermSet $ PB.putField $ PB.TermSet $ PB.putField $ setValueToPb s <$> Set.toList vs
Variable v -> absurd v
Antiquote v -> absurd v
pbToSetValue :: Symbols -> PB.TermV2 -> Either String (Term' 'WithinSet 'InFact 'Representation)
pbToSetValue s = \case
PB.TermInteger f -> pure $ LInteger $ fromIntegral $ PB.getField f
PB.TermString f -> LString <$> getSymbol s (SymbolRef $ PB.getField f)
PB.TermDate f -> pure $ LDate $ pbTimeToUtcTime $ PB.getField f
PB.TermBytes f -> pure $ LBytes $ PB.getField f
PB.TermBool f -> pure $ LBool $ PB.getField f
PB.TermVariable _ -> Left "Variables can't appear in facts or sets"
PB.TermTermSet _ -> Left "Sets can't be nested"
setValueToPb :: ReverseSymbols -> Term' 'WithinSet 'InFact 'Representation -> PB.TermV2
setValueToPb s = \case
LInteger v -> PB.TermInteger $ PB.putField $ fromIntegral v
LString v -> PB.TermString $ PB.putField $ getSymbolRef $ getSymbolCode s v
LDate v -> PB.TermDate $ PB.putField $ round $ utcTimeToPOSIXSeconds v
LBytes v -> PB.TermBytes $ PB.putField v
LBool v -> PB.TermBool $ PB.putField v
TermSet v -> absurd v
Variable v -> absurd v
Antiquote v -> absurd v
pbToExpression :: Symbols -> PB.ExpressionV2 -> Either String Expression
pbToExpression s PB.ExpressionV2{ops} = do
parsedOps <- traverse (pbToOp s) $ PB.getField ops
fromStack parsedOps
expressionToPb :: ReverseSymbols -> Expression -> PB.ExpressionV2
expressionToPb s e =
let ops = opToPb s <$> toStack e
in PB.ExpressionV2 { ops = PB.putField ops }
pbToOp :: Symbols -> PB.Op -> Either String Op
pbToOp s = \case
PB.OpVValue v -> VOp <$> pbToTerm s (PB.getField v)
PB.OpVUnary v -> pure . UOp . pbToUnary $ PB.getField v
PB.OpVBinary v -> pure . BOp . pbToBinary $ PB.getField v
opToPb :: ReverseSymbols -> Op -> PB.Op
opToPb s = \case
VOp t -> PB.OpVValue $ PB.putField $ termToPb s t
UOp o -> PB.OpVUnary $ PB.putField $ unaryToPb o
BOp o -> PB.OpVBinary $ PB.putField $ binaryToPb o
pbToUnary :: PB.OpUnary -> Unary
pbToUnary PB.OpUnary{kind} = case PB.getField kind of
PB.Negate -> Negate
PB.Parens -> Parens
PB.Length -> Length
unaryToPb :: Unary -> PB.OpUnary
unaryToPb = PB.OpUnary . PB.putField . \case
Negate -> PB.Negate
Parens -> PB.Parens
Length -> PB.Length
pbToBinary :: PB.OpBinary -> Binary
pbToBinary PB.OpBinary{kind} = case PB.getField kind of
PB.LessThan -> LessThan
PB.GreaterThan -> GreaterThan
PB.LessOrEqual -> LessOrEqual
PB.GreaterOrEqual -> GreaterOrEqual
PB.Equal -> Equal
PB.Contains -> Contains
PB.Prefix -> Prefix
PB.Suffix -> Suffix
PB.Regex -> Regex
PB.Add -> Add
PB.Sub -> Sub
PB.Mul -> Mul
PB.Div -> Div
PB.And -> And
PB.Or -> Or
PB.Intersection -> Intersection
PB.Union -> Union
PB.BitwiseAnd -> BitwiseAnd
PB.BitwiseOr -> BitwiseOr
PB.BitwiseXor -> BitwiseXor
binaryToPb :: Binary -> PB.OpBinary
binaryToPb = PB.OpBinary . PB.putField . \case
LessThan -> PB.LessThan
GreaterThan -> PB.GreaterThan
LessOrEqual -> PB.LessOrEqual
GreaterOrEqual -> PB.GreaterOrEqual
Equal -> PB.Equal
Contains -> PB.Contains
Prefix -> PB.Prefix
Suffix -> PB.Suffix
Regex -> PB.Regex
Add -> PB.Add
Sub -> PB.Sub
Mul -> PB.Mul
Div -> PB.Div
And -> PB.And
Or -> PB.Or
Intersection -> PB.Intersection
Union -> PB.Union
BitwiseAnd -> PB.BitwiseAnd
BitwiseOr -> PB.BitwiseOr
BitwiseXor -> PB.BitwiseXor
pbToThirdPartyBlockRequest :: PB.ThirdPartyBlockRequest -> Either String (Crypto.PublicKey, [Crypto.PublicKey])
pbToThirdPartyBlockRequest PB.ThirdPartyBlockRequest{previousPk, pkTable} = do
bisequence
( pbToPublicKey $ PB.getField previousPk
, traverse pbToPublicKey $ PB.getField pkTable
)
thirdPartyBlockRequestToPb :: (Crypto.PublicKey, [Crypto.PublicKey]) -> PB.ThirdPartyBlockRequest
thirdPartyBlockRequestToPb (previousPk, pkTable) = PB.ThirdPartyBlockRequest
{ previousPk = PB.putField $ publicKeyToPb previousPk
, pkTable = PB.putField $ publicKeyToPb <$> pkTable
}
pbToThirdPartyBlockContents :: PB.ThirdPartyBlockContents -> Either String (ByteString, Crypto.Signature, Crypto.PublicKey)
pbToThirdPartyBlockContents PB.ThirdPartyBlockContents{payload,externalSig} = do
(sig, pk) <- pbToOptionalSignature $ PB.getField externalSig
pure ( PB.getField payload
, sig
, pk
)
thirdPartyBlockContentsToPb :: (ByteString, Crypto.Signature, Crypto.PublicKey) -> PB.ThirdPartyBlockContents
thirdPartyBlockContentsToPb (payload, sig, pk) = PB.ThirdPartyBlockContents
{ PB.payload = PB.putField payload
, PB.externalSig = PB.putField $ externalSigToPb (sig, pk)
}