biscuit-servant 0.1.1.0 → 0.2.0.0
raw patch · 6 files changed
+521/−186 lines, 6 filesdep +timedep ~biscuit-haskellPVP ok
version bump matches the API change (PVP)
Dependencies added: time
Dependency ranges changed: biscuit-haskell
API changes (from Hackage documentation)
- Auth.Biscuit.Servant: WithVerifier :: ReaderT Biscuit m a -> Verifier -> WithVerifier m a
- Auth.Biscuit.Servant: [verifier_] :: WithVerifier m a -> Verifier
- Auth.Biscuit.Servant: data WithVerifier m a
- Auth.Biscuit.Servant: noVerifier :: Monad m => ReaderT Biscuit m a -> WithVerifier m a
- Auth.Biscuit.Servant: noVerifier_ :: Monad m => m a -> WithVerifier m a
- Auth.Biscuit.Servant: withFallbackVerifier :: Verifier -> WithVerifier m a -> WithVerifier m a
- Auth.Biscuit.Servant: withPriorityVerifier :: Verifier -> WithVerifier m a -> WithVerifier m a
- Auth.Biscuit.Servant: withVerifier :: Monad m => Verifier -> ReaderT Biscuit m a -> WithVerifier m a
- Auth.Biscuit.Servant: withVerifier_ :: Monad m => Verifier -> m a -> WithVerifier m a
+ Auth.Biscuit.Servant: WithAuthorizer :: ReaderT (Biscuit OpenOrSealed Verified) m a -> m Authorizer -> WithAuthorizer m a
+ Auth.Biscuit.Servant: [authorizer_] :: WithAuthorizer m a -> m Authorizer
+ Auth.Biscuit.Servant: checkBiscuitM :: (MonadIO m, MonadError ServerError m) => Biscuit OpenOrSealed Verified -> m Authorizer -> m a -> m a
+ Auth.Biscuit.Servant: data WithAuthorizer m a
+ Auth.Biscuit.Servant: noAuthorizer :: Applicative m => ReaderT (Biscuit OpenOrSealed Verified) m a -> WithAuthorizer m a
+ Auth.Biscuit.Servant: noAuthorizer_ :: Monad m => m a -> WithAuthorizer m a
+ Auth.Biscuit.Servant: withAuthorizer :: Applicative m => Authorizer -> ReaderT (Biscuit OpenOrSealed Verified) m a -> WithAuthorizer m a
+ Auth.Biscuit.Servant: withAuthorizerM :: m Authorizer -> ReaderT (Biscuit OpenOrSealed Verified) m a -> WithAuthorizer m a
+ Auth.Biscuit.Servant: withAuthorizerM_ :: Monad m => m Authorizer -> m a -> WithAuthorizer m a
+ Auth.Biscuit.Servant: withAuthorizer_ :: Monad m => Authorizer -> m a -> WithAuthorizer m a
+ Auth.Biscuit.Servant: withFallbackAuthorizer :: Functor m => Authorizer -> WithAuthorizer m a -> WithAuthorizer m a
+ Auth.Biscuit.Servant: withFallbackAuthorizerM :: Applicative m => m Authorizer -> WithAuthorizer m a -> WithAuthorizer m a
+ Auth.Biscuit.Servant: withPriorityAuthorizer :: Functor m => Authorizer -> WithAuthorizer m a -> WithAuthorizer m a
+ Auth.Biscuit.Servant: withPriorityAuthorizerM :: Applicative m => m Authorizer -> WithAuthorizer m a -> WithAuthorizer m a
- Auth.Biscuit.Servant: [handler_] :: WithVerifier m a -> ReaderT Biscuit m a
+ Auth.Biscuit.Servant: [handler_] :: WithAuthorizer m a -> ReaderT (Biscuit OpenOrSealed Verified) m a
- Auth.Biscuit.Servant: authHandler :: PublicKey -> AuthHandler Request CheckedBiscuit
+ Auth.Biscuit.Servant: authHandler :: PublicKey -> AuthHandler Request (Biscuit OpenOrSealed Verified)
- Auth.Biscuit.Servant: checkBiscuit :: (MonadIO m, MonadError ServerError m) => CheckedBiscuit -> Verifier -> m a -> m a
+ Auth.Biscuit.Servant: checkBiscuit :: (MonadIO m, MonadError ServerError m) => Biscuit OpenOrSealed Verified -> Authorizer -> m a -> m a
- Auth.Biscuit.Servant: genBiscuitCtx :: PublicKey -> Context '[AuthHandler Request CheckedBiscuit]
+ Auth.Biscuit.Servant: genBiscuitCtx :: PublicKey -> Context '[AuthHandler Request (Biscuit OpenOrSealed Verified)]
- Auth.Biscuit.Servant: handleBiscuit :: (MonadIO m, MonadError ServerError m) => CheckedBiscuit -> WithVerifier m a -> m a
+ Auth.Biscuit.Servant: handleBiscuit :: (MonadIO m, MonadError ServerError m) => Biscuit OpenOrSealed Verified -> WithAuthorizer m a -> m a
Files
- README.md +53/−1
- biscuit-servant.cabal +4/−3
- src/Auth/Biscuit/Servant.hs +368/−103
- test/AppWithAuthorizer.hs +66/−0
- test/AppWithVerifier.hs +0/−60
- test/Spec.hs +30/−19
README.md view
@@ -1,1 +1,53 @@-# Biscuit-based auth for servant apps+<img src="https://raw.githubusercontent.com/divarvel/biscuit-haskell/main/assets/biscuit-logo.png" align=right>++# biscuit-servant 🤖 [![Hackage][hackage]][hackage-url]++> **Servant combinators to enable biscuit validation in your API trees**++## Usage++```Haskell+type AppM = WithAuthorizer Handler+type API = RequireBiscuit :> ProtectedAPI++-- /users+-- /users/:userId+type ProtectedAPI =+ "users" :> ( Get '[JSON] [User]+ :<|> Capture "userId" Int :> Get '[JSON] User+ )+app :: PublicKey -> Application+app pk = serveWithContext @API Proxy (genBiscuitCtx pk) server++server :: Server API+server biscuit =+ let handlers = userListHandler :<|> singleUserHandler+ handleAuth =+ handleBiscuit biscuit+ -- `allow if right("admin");` will be the first policy+ -- for every endpoint.+ -- Policies added by endpoints (or sub-apis) will tried after this one.+ . withPriorityAuthorizer [authorizer|allow if right("admin");|]+ -- `deny if true;` will be the last policy for every endpoint.+ -- Policies added by endpoints (or sub-apis) will tried before this one.+ . withFallbackAuthorizer [authorizer|deny if true;|]+ in hoistServer @ProtectedAPI Proxy handleAuth handlers++allUsers :: [User]+allUsers = [ User 1 "Danielle" "George"+ , User 2 "Albert" "Einstein"+ ]++userListHandler :: AppM [User]+userListHandler = withAuthorizer [authorizer|allow if right("userList")|]+ $ pure allUsers++singleUserHandler :: Int -> AppM User+singleUserHandler uid =+ withAuthorizer [authorizer|allow if right("getUser", ${uid})|] $+ let user = find (\user -> userId user == uid) allUsers+ in maybe (throwError error404) (\user -> pure user) user+```++[Hackage]: https://img.shields.io/hackage/v/biscuit-haskell?color=purple&style=flat-square+[hackage-url]: https://hackage.haskell.org/package/biscuit-servant
biscuit-servant.cabal view
@@ -1,7 +1,7 @@ cabal-version: 2.0 name: biscuit-servant-version: 0.1.1.0+version: 0.2.0.0 category: Security synopsis: Servant support for the Biscuit security token description: Please see the README on GitHub at <https://github.com/divarvel/biscuit-haskell#readme>@@ -33,7 +33,7 @@ ghc-options: -Wall build-depends: base >= 4.7 && <5,- biscuit-haskell ^>= 0.1,+ biscuit-haskell ^>= 0.2, bytestring ^>= 0.10, mtl ^>= 2.2, text ^>= 1.2,@@ -45,7 +45,7 @@ type: exitcode-stdio-1.0 main-is: Spec.hs other-modules:- AppWithVerifier+ AppWithAuthorizer ClientHelpers hs-source-dirs: test@@ -62,5 +62,6 @@ , servant-client , servant-client-core , text+ , time , warp default-language: Haskell2010
src/Auth/Biscuit/Servant.hs view
@@ -5,25 +5,38 @@ {-# LANGUAGE TypeFamilies #-} module Auth.Biscuit.Servant (- -- Servant Auth Handler+ -- * Protecting a servant API with biscuits+ -- $presentation++ -- ** Annotating servant API types+ -- $apitypes RequireBiscuit , authHandler , genBiscuitCtx+ -- ** Supplying a authorizer for a single endpoint+ -- $singleEndpointAuthorizer , checkBiscuit- -- Decorate regular handlers with composable verifiers- , WithVerifier (..)+ , checkBiscuitM+ -- ** Decorate regular handlers with composable authorizers+ -- $composableAuthorizers+ , WithAuthorizer (..) , handleBiscuit- , withVerifier- , withVerifier_- , noVerifier- , noVerifier_- , withFallbackVerifier- , withPriorityVerifier+ , withAuthorizer+ , withAuthorizer_+ , withAuthorizerM+ , withAuthorizerM_+ , noAuthorizer+ , noAuthorizer_+ , withFallbackAuthorizer+ , withPriorityAuthorizer+ , withFallbackAuthorizerM+ , withPriorityAuthorizerM++ , module Biscuit ) where -import Auth.Biscuit (Biscuit, PublicKey, Verifier,- checkBiscuitSignature,- parseB64, verifyBiscuit)+import Auth.Biscuit as Biscuit+import Control.Applicative (liftA2) import Control.Monad.Except (MonadError, throwError) import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.Reader (ReaderT, lift, runReaderT)@@ -36,6 +49,143 @@ import Servant.Server import Servant.Server.Experimental.Auth +-- $presentation+--+-- Biscuit are bearer tokens that can be used to protect API endpoints.+-- This package provides utilities to protect servant endpoints with such+-- tokens.+--+-- The token will be extracted from the @Authorization@ header, and must+-- be base64-encoded, prefixed with the @Bearer @ string.++-- $apitypes+--+-- To protect and endpoint (or a whole API tree), you can use 'RequireBiscuit'+-- like so:+--+--+-- > type API = RequireBiscuit :> ProtectedAPI+-- > type ProtectedAPI =+-- > "endpoint1" :> Get '[JSON] Int+-- > :<|> "endpoint2" :> Capture "int" Int :> Get '[JSON] Int+-- > :<|> "endpoint3" :> Get '[JSON] Int+-- >+-- > app :: PublicKey -> Application+-- > app publicKey =+-- > -- servant needs access to the biscuit /public/+-- > -- key to be able to check biscuit signatures.+-- > -- The public key can be read from the environment+-- > -- and parsed using 'parsePublicKeyHex' for instance.+-- > serveWithContext+-- > (Proxy :: Proxy API)+-- > (genBiscuitCtx publicKey)+-- > server+-- >+-- > -- server :: Biscuit OpenOrSealed Verified -> Server ProtectedAPI+-- > server :: Server API+-- > server biscuit = … -- this will be detailed later+--+-- This will instruct servant to extract the biscuit from the requests and+-- check its signature. /It will not/, however, run any datalog check (as+-- the checks typically depend on the request contents).+--+-- $singleEndpointAuthorizer+--+-- The corresponding @Server API@ value will be a @Biscuit OpenOrSealed Verified -> Server ProtectedAPI@.+-- The next step is to provide a 'Authorizer' so that the biscuit datalog can be+-- verified. For that, you can use 'checkBiscuit' (or 'checkBiscuitM' for effectful checks).+--+-- > server :: Server API+-- > server biscuit = h1 biscuit+-- > :<|> h2 biscuit+-- > :<|> h3 biscuit+-- >+-- > h1 :: Biscuit OpenOrSealed Verified -> Handler Int+-- > h1 biscuit =+-- > checkBiscuit biscuit+-- > [authorizer|allow if right("one");|]+-- > -- ^ only allow biscuits granting access to the endpoint tagged "one"+-- > (pure 1)+-- >+-- > h2 :: Biscuit OpenOrSealed Verified -> Int -> Handler Int+-- > h2 biscuit value =+-- > let authorizer' = do+-- > now <- liftIO getCurrentTime+-- > pure [authorizer|+-- > // provide the current time so that TTL checks embedded in+-- > // the biscuit can decide if it's still valid+-- > // this show how to run an effectful check with+-- > // checkBiscuitM (getting the current time is an effect)+-- > time(${now});+-- > // only allow biscuits granting access to the endpoint tagged "two"+-- > // AND for the provided int value. This show how the checks can depend+-- > // on the http request contents.+-- > allow if right("two", ${value});+-- > |]+-- > checkBiscuitM biscuit authorizer+-- > (pure 2)+-- >+-- > h3 :: Biscuit OpenOrSealed Verified -> Handler Int+-- > h3 biscuit =+-- > checkBiscuit biscuit+-- > [authorizer|deny if true;|]+-- > -- ^ reject every biscuit+-- > (pure 3)+--+-- $composableAuthorizers+--+-- 'checkBiscuit' allows you to describe validation rules endpoint by endpoint. If your+-- application has a lot of endpoints with the same policies, it can become tedious to+-- maintain.+--+-- 'biscuit-servant' provides a way to apply authorizers on whole API trees,+-- in a composable way, thanks to 'hoistServer'. 'hoistServer' is a mechanism+-- provided by servant-server that lets apply a transformation function to whole+-- API trees.+--+-- > -- 'withAuthorizer' wraps a 'Handler' and lets you attach a authorizer to a+-- > -- specific endoint. This authorizer may be combined with other authorizers+-- > -- attached to the whole API tree+-- > handler1 :: WithAuthorizer Handler Int+-- > handler1 = withAuthorizer+-- > [authorizer|allow if right("one");|]+-- > (pure 1)+-- >+-- > handler2 :: Int -> WithAuthorizer Handler Int+-- > handler2 value = withAuthorizer+-- > [authorizer|allow if right("two", ${value});|]+-- > (pure 2)+-- >+-- > handler3 :: WithAuthorizer Handler Int+-- > handler3 = withAuthorizer+-- > [authorizer|allow if right("three");|]+-- > (pure 3)+-- >+-- > server :: Biscuit OpenOrSealed Verified -> Server ProtectedAPI+-- > server biscuit =+-- > let nowFact = do+-- > now <- liftIO getCurrentTime+-- > pure [authorizer|time(${now});|]+-- > handleAuth :: WithAuthorizer Handler x -> Handler x+-- > handleAuth =+-- > handleBiscuit biscuit+-- > -- ^ this runs datalog checks on the biscuit, based on authorizers attached to+-- > -- the handlers+-- > . withPriorityAuthorizerM nowFact+-- > -- ^ this provides the current time to the verification context so that biscuits with+-- > -- a TTL can check if they are still valid.+-- > -- Authorizers can be provided in a monadic context (it has to be the same monad as+-- > -- the handlers themselves, so here it's 'Handler').+-- > . withPriorityAuthorizer [authorizer|allow if right("admin");|]+-- > -- ^ this policy will be tried /before/ any endpoint policy, so `endpoint3` will be+-- > -- reachable with an admin biscuit+-- > . withFallbackAuthorizer [authorizer|allow if right("anon");|]+-- > -- ^ this policy will be tried /after/ the endpoints policies, so `endpoint3` will+-- > -- *not* be reachable with an anon macaroon.+-- > handlers = handler1 :<|> handler2 :<|> handler3+-- > in hoistServer @ProtectedAPI Proxy handleAuth handlers+-- > -- ^ this will apply `handleAuth` on all 'ProtectedAPI' endpoints.+ -- | Type used to protect and API tree, requiring a biscuit token -- to be attached to requests. The associated auth handler will -- only check the biscuit signature. Checking the datalog part@@ -43,147 +193,262 @@ -- be performed separately with either 'checkBiscuit' (for simple -- use-cases) or 'handleBiscuit' (for more complex use-cases). type RequireBiscuit = AuthProtect "biscuit"-type instance AuthServerData RequireBiscuit = CheckedBiscuit---- | A biscuit which signature has already been verified.--- Since the biscuit lib checks the signature while verifying the datalog--- part, the public key is needed. 'CheckedBiscuit' carries the public key--- used for verifying the signature so that the datalog verification part--- can use it.-data CheckedBiscuit = CheckedBiscuit PublicKey Biscuit+type instance AuthServerData RequireBiscuit = Biscuit OpenOrSealed Verified --- | Wrapper for a servant handler, equipped with a biscuit 'Verifier'+-- | Wrapper for a servant handler, equipped with a biscuit 'Authorizer' -- that will be used to authorize the request. If the authorization -- succeeds, the handler is ran. -- The handler itself is given access to the verified biscuit through--- a 'ReaderT Biscuit'.-data WithVerifier m a- = WithVerifier- { handler_ :: ReaderT Biscuit m a+-- a @ReaderT (Biscuit OpenOrSealed Verified)@.+data WithAuthorizer m a+ = WithAuthorizer+ { handler_ :: ReaderT (Biscuit OpenOrSealed Verified) m a -- ^ the wrapped handler, in a 'ReaderT' to give easy access to the biscuit- , verifier_ :: Verifier- -- ^ the 'Verifier' associated to the handler+ , authorizer_ :: m Authorizer+ -- ^ the 'Authorizer' associated to the handler } --- | Combines the provided 'Verifier' to the 'Verifier' attached to the wrapped--- handler. _facts_, _rules_ and _checked_ are unordered, but _policies_ have a--- specific order. 'withFallbackVerifier' puts the provided policies at the _bottom_--- of the list (ie as _fallback_ policies).+-- | Combines the provided 'Authorizer' to the 'Authorizer' attached to the wrapped+-- handler. /facts/, /rules/ and /checks/ are unordered, but /policies/ have a+-- specific order. 'withFallbackAuthorizer' puts the provided policies at the /bottom/+-- of the list (ie as /fallback/ policies): these policies will be tried /after/+-- the policies declared through 'withPriorityAuthorizer' and after the policies+-- declared by the endpoints.+-- -- If you want the policies to be tried before the ones of the wrapped handler, you--- can use 'withPriorityVerifier'.-withFallbackVerifier :: Verifier- -> WithVerifier m a- -> WithVerifier m a-withFallbackVerifier newV h@WithVerifier{verifier_} =- h { verifier_ = verifier_ <> newV }+-- can use 'withPriorityAuthorizer'.+--+-- If you need to perform effects to compute the authorizer (eg. to get the current date,+-- or to query a database), you can use 'withFallbackAuthorizerM' instead.+withFallbackAuthorizer :: Functor m+ => Authorizer+ -> WithAuthorizer m a+ -> WithAuthorizer m a+withFallbackAuthorizer newV h@WithAuthorizer{authorizer_} =+ h { authorizer_ = (<> newV) <$> authorizer_ } --- | Combines the provided 'Verifier' to the 'Verifier' attached to the wrapped--- handler. _facts_, _rules_ and _checked_ are unordered, but _policies_ have a--- specific order. 'withFallbackVerifier' puts the provided policies at the _top_--- of the list (ie as _priority_ policies).+-- | Combines the provided 'Authorizer' to the 'Authorizer' attached to the wrapped+-- handler. /facts/, /rules/ and /checks/ are unordered, but /policies/ have a+-- specific order. 'withFallbackAuthorizer' puts the provided policies at the /bottom/+-- of the list (ie as /fallback/ policies): these policies will be tried /after/+-- the policies declared through 'withPriorityAuthorizer' and after the policies+-- declared by the endpoints.+--+-- If you want the policies to be tried before the ones of the wrapped handler, you+-- can use 'withPriorityAuthorizer'.+--+-- Here, the 'Authorizer' can be computed effectfully. If you don't need to perform effects,+-- you can use 'withFallbackAuthorizer' instead.+withFallbackAuthorizerM :: Applicative m+ => m Authorizer+ -> WithAuthorizer m a+ -> WithAuthorizer m a+withFallbackAuthorizerM newV h@WithAuthorizer{authorizer_} =+ h { authorizer_ = liftA2 (<>) authorizer_ newV }++-- | Combines the provided 'Authorizer' to the 'Authorizer' attached to the wrapped+-- handler. /facts/, /rules/ and /checks/ are unordered, but /policies/ have a+-- specific order. 'withFallbackAuthorizer' puts the provided policies at the /top/+-- of the list (ie as /priority/ policies): these policies will be tried /after/+-- the policies declared through 'withPriorityAuthorizer' and after the policies+-- declared by the endpoints.+-- -- If you want the policies to be tried after the ones of the wrapped handler, you--- can use 'withFallbackVerifier'.-withPriorityVerifier :: Verifier- -> WithVerifier m a- -> WithVerifier m a-withPriorityVerifier newV h@WithVerifier{verifier_} =- h { verifier_ = newV <> verifier_ }+-- can use 'withFallbackAuthorizer'.+--+-- If you need to perform effects to compute the authorizer (eg. to get the current date,+-- or to query a database), you can use 'withPriorityAuthorizerM' instead.+withPriorityAuthorizer :: Functor m+ => Authorizer+ -> WithAuthorizer m a+ -> WithAuthorizer m a+withPriorityAuthorizer newV h@WithAuthorizer{authorizer_} =+ h { authorizer_ = (newV <>) <$> authorizer_ } --- | Wraps an existing handler block, attaching a 'Verifier'. The handler has--- to be a 'ReaderT Biscuit' to be able to access the token. If you don't need--- to access the token from the handler block, you can use 'withVerifier_'--- instead.-withVerifier :: Monad m => Verifier -> ReaderT Biscuit m a -> WithVerifier m a-withVerifier verifier_ handler_ =- WithVerifier+-- | Combines the provided 'Authorizer' to the 'Authorizer' attached to the wrapped+-- handler. /facts/, /rules/ and /checks/ are unordered, but /policies/ have a+-- specific order. 'withFallbackAuthorizer' puts the provided policies at the /top/+-- of the list (ie as /priority/ policies): these policies will be tried /after/+-- the policies declared through 'withPriorityAuthorizer' and after the policies+-- declared by the endpoints.+--+-- If you want the policies to be tried after the ones of the wrapped handler, you+-- can use 'withFallbackAuthorizer'.+--+-- Here, the 'Authorizer' can be computed effectfully. If you don't need to perform effects,+-- you can use 'withFallbackAuthorizer' instead.+withPriorityAuthorizerM :: Applicative m+ => m Authorizer+ -> WithAuthorizer m a+ -> WithAuthorizer m a+withPriorityAuthorizerM newV h@WithAuthorizer{authorizer_} =+ h { authorizer_ = liftA2 (<>) newV authorizer_ }++-- | Wraps an existing handler block, attaching a 'Authorizer'. The handler has+-- to be a @ReaderT (Biscuit OpenOrSealed Verified)' to be able to access the token.+-- If you don't need to access the token from the handler block, you can use+-- 'withAuthorizer_' instead.+--+-- If you need to perform effects to compute the authorizer (eg. to get the current date,+-- or to query a database), you can use 'withAuthorizerM' instead.+withAuthorizer :: Applicative m+ => Authorizer+ -> ReaderT (Biscuit OpenOrSealed Verified) m a+ -> WithAuthorizer m a+withAuthorizer v handler_ =+ WithAuthorizer { handler_- , verifier_+ , authorizer_ = pure v } --- | Wraps an existing handler block, attaching a 'Verifier'. The handler can be--- any monad, but won't be able to access the 'Biscuit'. If you want to read the--- biscuit token from the handler block, you can use 'withVerifier' instead.-withVerifier_ :: Monad m => Verifier -> m a -> WithVerifier m a-withVerifier_ v = withVerifier v . lift+-- | Wraps an existing handler block, attaching a 'Authorizer'. The handler has+-- to be a @ReaderT (Biscuit OpenOrSealed Verified)@ to be able to access the token.+-- If you don't need to access the token from the handler block, you can use+-- 'withAuthorizer_' instead.+--+-- Here, the 'Authorizer' can be computed effectfully. If you don't need to perform effects,+-- you can use 'withAuthorizer' instead.+withAuthorizerM :: m Authorizer+ -> ReaderT (Biscuit OpenOrSealed Verified) m a+ -> WithAuthorizer m a+withAuthorizerM authorizer_ handler_ =+ WithAuthorizer+ { handler_+ , authorizer_+ } --- | Wraps an existing handler block, attaching an empty 'Verifier'. The handler has--- to be a 'ReaderT Biscuit' to be able to access the token. If you don't need--- to access the token from the handler block, you can use 'noVerifier_'+-- | Wraps an existing handler block, attaching a 'Authorizer'. The handler can be+-- any monad, but won't be able to access the biscuit. If you want to read the biscuit+-- token from the handler block, you can use 'withAuthorizer' instead.+--+-- If you need to perform effects to compute the authorizer (eg. to get the current date,+-- or to query a database), you can use 'withAuthorizerM_' instead.+withAuthorizer_ :: Monad m => Authorizer -> m a -> WithAuthorizer m a+withAuthorizer_ v = withAuthorizer v . lift++-- | Wraps an existing handler block, attaching a 'Authorizer'. The handler can be+-- any monad, but won't be able to access the 'Biscuit'.+--+-- If you want to read the biscuit token from the handler block, you can use 'withAuthorizer' -- instead. ----- This function can be used together with 'withFallbackVerifier' or 'withPriorityVerifier'--- to apply policies on several handlers at the same time (with 'hoistServer' for instance).-noVerifier :: Monad m => ReaderT Biscuit m a -> WithVerifier m a-noVerifier = withVerifier mempty+-- Here, the 'Authorizer' can be computed effectfully. If you don't need to perform effects,+-- you can use 'withAuthorizer_' instead.+withAuthorizerM_ :: Monad m => m Authorizer -> m a -> WithAuthorizer m a+withAuthorizerM_ v = withAuthorizerM v . lift --- | Wraps an existing handler block, attaching an empty 'Verifier'. The handler can be--- any monad, but won't be able to access the 'Biscuit'. If you want to read the--- biscuit token from the handler block, you can use 'noVerifier' instead.+-- | Wraps an existing handler block, attaching an empty 'Authorizer'. The handler has+-- to be a @ReaderT (Biscuit OpenOrSealed Verified)@ to be able to access the token. If you don't need+-- to access the token from the handler block, you can use 'noAuthorizer_'+-- instead. ----- This function can be used together with 'withFallbackVerifier' or 'withPriorityVerifier'--- to apply policies on several handlers at the same time (with 'hoistServer' for instance).-noVerifier_ :: Monad m => m a -> WithVerifier m a-noVerifier_ = noVerifier . lift+-- This function is useful when the endpoint does not have any specific authorizer+-- context, and the authorizer context is applied on the whole API tree through+-- 'withFallbackAuthorizer' or 'withPriorityAuthorizer' to apply policies on several+-- handlers at the same time (with 'hoistServer' for instance).+noAuthorizer :: Applicative m+ => ReaderT (Biscuit OpenOrSealed Verified) m a+ -> WithAuthorizer m a+noAuthorizer = withAuthorizer mempty +-- | Wraps an existing handler block, attaching an empty 'Authorizer'. The handler can be+-- any monad, but won't be able to access the biscuit. If you want to read the+-- biscuit token from the handler block, you can use 'noAuthorizer' instead.+--+-- This function is useful when the endpoint does not have any specific authorizer+-- context, and the authorizer context is applied on the whole API tree through+-- 'withFallbackAuthorizer' or 'withPriorityAuthorizer' to apply policies on several+-- handlers at the same time (with 'hoistServer' for instance).+noAuthorizer_ :: Monad m => m a -> WithAuthorizer m a+noAuthorizer_ = noAuthorizer . lift+ -- | Extracts a biscuit from an http request, assuming: -- -- - the biscuit is b64-encoded--- - prefixed with the `Bearer ` string--- - in the `Authorization` header-extractBiscuit :: Request -> Either String Biscuit-extractBiscuit req = do+-- - prefixed with the @Bearer @ string+-- - in the @Authorization@ header+extractBiscuit :: PublicKey+ -> Request+ -> Either String (Biscuit OpenOrSealed Verified)+extractBiscuit pk req = do let note e = maybe (Left e) Right authHeader <- note "Missing Authorization header" . lookup "Authorization" $ requestHeaders req b64Token <- note "Not a Bearer token" $ BS.stripPrefix "Bearer " authHeader- first (const "Not a B64-encoded biscuit") $ parseB64 b64Token+ first (const "Not a B64-encoded biscuit") $ parseB64 pk b64Token -- | Servant authorization handler. This extracts the biscuit from the request,--- checks its signature (but not the datalog part) and returns a 'CheckedBiscuit'+-- checks its signature (but not the datalog part) and returns a 'Biscuit' -- upon success.-authHandler :: PublicKey -> AuthHandler Request CheckedBiscuit+authHandler :: PublicKey+ -> AuthHandler Request (Biscuit OpenOrSealed Verified) authHandler publicKey = mkAuthHandler handler where authError s = err401 { errBody = LBS.fromStrict (C8.pack s) } orError = either (throwError . authError) pure- handler req = do- biscuit <- orError $ extractBiscuit req- result <- liftIO $ checkBiscuitSignature biscuit publicKey- case result of- False -> throwError $ authError "Invalid signature"- True -> pure $ CheckedBiscuit publicKey biscuit+ handler req =+ orError $ extractBiscuit publicKey req -- | Helper function generating a servant context containing the authorization -- handler.-genBiscuitCtx :: PublicKey -> Context '[AuthHandler Request CheckedBiscuit]+genBiscuitCtx :: PublicKey+ -> Context '[AuthHandler Request (Biscuit OpenOrSealed Verified)] genBiscuitCtx pk = authHandler pk :. EmptyContext --- | Given a 'CheckedBiscuit' (provided by the servant authorization mechanism),--- verify its validity (with the provided 'Verifier'). If you don't want to pass--- the biscuit manually to all the endpoints or want to blanket apply verifiers on--- whole API trees, you can consider using 'withVerifier' (on endpoints), 'withFallbackVerifier' and--- 'withPriorityVerifier' (on API sub-trees) and 'handleBiscuit' (on the whole API).+-- | Given a biscuit (provided by the servant authorization mechanism),+-- verify its validity (with the provided 'Authorizer').+--+-- If you need to perform effects in the verification phase (eg to get the current time,+-- or if you need to issue a DB query to retrieve extra information needed to check the token),+-- you can use 'checkBiscuitM' instead.+--+-- If you don't want to pass the biscuit manually to all the endpoints or want to+-- blanket apply authorizers on whole API trees, you can consider using 'withAuthorizer'+-- (on endpoints), 'withFallbackAuthorizer' and 'withPriorityAuthorizer' (on API sub-trees)+-- and 'handleBiscuit' (on the whole API). checkBiscuit :: (MonadIO m, MonadError ServerError m)- => CheckedBiscuit- -> Verifier+ => Biscuit OpenOrSealed Verified+ -> Authorizer -> m a -> m a-checkBiscuit (CheckedBiscuit pk b) v h = do- res <- liftIO $ verifyBiscuit b v pk+checkBiscuit vb v h = do+ res <- liftIO $ authorizeBiscuit vb v case res of Left e -> do liftIO $ print e throwError $ err401 { errBody = "Biscuit failed checks" } Right _ -> h --- | Given a handler wrapped in a 'WithVerifier', use the attached 'Verifier' to+-- | Given a 'Biscuit' (provided by the servant authorization mechanism),+-- verify its validity (with the provided 'Authorizer', which can be effectful).+--+-- If you don't need to run any effects in the verifying phase, you can use 'checkBiscuit'+-- instead.+--+-- If you don't want to pass the biscuit manually to all the endpoints or want to blanket apply+-- authorizers on whole API trees, you can consider using 'withAuthorizer' (on endpoints),+-- 'withFallbackAuthorizer' and 'withPriorityAuthorizer' (on API sub-trees) and 'handleBiscuit'+-- (on the whole API).+checkBiscuitM :: (MonadIO m, MonadError ServerError m)+ => Biscuit OpenOrSealed Verified+ -> m Authorizer+ -> m a+ -> m a+checkBiscuitM vb mv h = do+ v <- mv+ res <- liftIO $ authorizeBiscuit vb v+ case res of+ Left e -> do liftIO $ print e+ throwError $ err401 { errBody = "Biscuit failed checks" }+ Right _ -> h++-- | Given a handler wrapped in a 'WithAuthorizer', use the attached 'Authorizer' to -- verify the provided biscuit and return an error as needed. -- -- For simpler use cases, consider using 'checkBiscuit' instead, which works on regular -- servant handlers. handleBiscuit :: (MonadIO m, MonadError ServerError m)- => CheckedBiscuit- -> WithVerifier m a+ => Biscuit OpenOrSealed Verified+ -> WithAuthorizer m a -> m a-handleBiscuit cb@(CheckedBiscuit _ b) WithVerifier{verifier_, handler_} =+handleBiscuit b WithAuthorizer{authorizer_, handler_} = let h = runReaderT handler_ b- in checkBiscuit cb verifier_ h-+ in checkBiscuitM b authorizer_ h
+ test/AppWithAuthorizer.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+module AppWithAuthorizer where++import Auth.Biscuit+import Auth.Biscuit.Servant+import Control.Monad.IO.Class (liftIO)+import Data.Text (Text)+import Data.Time (getCurrentTime)+import Servant+import Servant.Client++import ClientHelpers++call1 :: Text -> ClientM Int+call1 b =+ let (e1 :<|> _) = client @API Proxy (protect b)+ in e1++call2 :: Text -> Int -> ClientM Int+call2 b =+ let (_ :<|> e2 :<|> _) = client @API Proxy (protect b)+ in e2++call3 :: Text -> ClientM Int+call3 b =+ let (_ :<|> _ :<|> e3) = client @API Proxy (protect b)+ in e3++type H = WithAuthorizer Handler+type API = RequireBiscuit :> ProtectedAPI+type ProtectedAPI =+ "endpoint1" :> Get '[JSON] Int+ :<|> "endpoint2" :> Capture "int" Int :> Get '[JSON] Int+ :<|> "endpoint3" :> Get '[JSON] Int++app :: PublicKey -> Application+app appPublicKey =+ serveWithContext @API Proxy (genBiscuitCtx appPublicKey) server++server :: Server API+server b =+ let nowFact = do+ now <- liftIO getCurrentTime+ pure [authorizer|time(${now});|]+ handleAuth :: WithAuthorizer Handler x -> Handler x+ handleAuth =+ handleBiscuit b+ . withPriorityAuthorizerM nowFact+ . withPriorityAuthorizer [authorizer|allow if right("admin");|]+ . withFallbackAuthorizer [authorizer|allow if right("anon");|]+ handlers = handler1 :<|> handler2 :<|> handler3+ in hoistServer @ProtectedAPI Proxy handleAuth handlers++handler1 :: H Int+handler1 = withAuthorizer [authorizer|allow if right("one");|] $ pure 1++handler2 :: Int -> H Int+handler2 v = withAuthorizer [authorizer|allow if right("two", ${v});|] $ pure 2++handler3 :: H Int+handler3 = withAuthorizer [authorizer|deny if true;|] $ pure 3
− test/AppWithVerifier.hs
@@ -1,60 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}-module AppWithVerifier where--import Auth.Biscuit-import Auth.Biscuit.Servant-import Data.Text (Text)-import Servant-import Servant.Client--import ClientHelpers--call1 :: Text -> ClientM Int-call1 b =- let (e1 :<|> _) = client @API Proxy (protect b)- in e1--call2 :: Text -> Int -> ClientM Int-call2 b =- let (_ :<|> e2 :<|> _) = client @API Proxy (protect b)- in e2--call3 :: Text -> ClientM Int-call3 b =- let (_ :<|> _ :<|> e3) = client @API Proxy (protect b)- in e3--type H = WithVerifier Handler-type API = RequireBiscuit :> ProtectedAPI-type ProtectedAPI =- "endpoint1" :> Get '[JSON] Int- :<|> "endpoint2" :> Capture "int" Int :> Get '[JSON] Int- :<|> "endpoint3" :> Get '[JSON] Int--app :: PublicKey -> Application-app appPublicKey =- serveWithContext @API Proxy (genBiscuitCtx appPublicKey) server--server :: Server API-server b =- let handleAuth :: WithVerifier Handler x -> Handler x- handleAuth =- handleBiscuit b- . withPriorityVerifier [verifier|allow if right(#authority, #admin);|]- . withFallbackVerifier [verifier|allow if right(#authority, #anon);|]- handlers = handler1 :<|> handler2 :<|> handler3- in hoistServer @ProtectedAPI Proxy handleAuth handlers--handler1 :: H Int-handler1 = withVerifier [verifier|allow if right(#authority, #one);|] $ pure 1--handler2 :: Int -> H Int-handler2 v = withVerifier [verifier|allow if right(#authority, #two, ${v});|] $ pure 2--handler3 :: H Int-handler3 = withVerifier [verifier|deny if true;|] $ pure 3
test/Spec.hs view
@@ -10,20 +10,24 @@ import Data.Maybe (fromJust) import Data.Text (Text) import Data.Text.Encoding (decodeUtf8)+import Data.Time (UTCTime, addUTCTime, getCurrentTime) import Test.Hspec -import AppWithVerifier (app, call1, call2, call3)+import AppWithAuthorizer (app, call1, call2, call3) import ClientHelpers (runC, withApp) main :: IO () main = do- keypair <- fromPrivateKey appPrivateKey- let appPk = publicKey keypair- adminB <- toText <$> mkAdminBiscuit keypair- anonB <- toText <$> mkAnonBiscuit keypair- e1 <- toText <$> mkE1Biscuit keypair- e21 <- toText <$> mkE2Biscuit 1 keypair- e22 <- toText <$> mkE2Biscuit 2 keypair+ let appPk = toPublic appSecretKey+ later <- addUTCTime (60*5) <$> getCurrentTime+ earlier <- addUTCTime (-60) <$> getCurrentTime+ adminB <- toText <$> mkAdminBiscuit appSecretKey+ anonB <- toText <$> mkAnonBiscuit appSecretKey+ e1 <- toText <$> mkE1Biscuit appSecretKey+ e21 <- toText <$> mkE2Biscuit 1 appSecretKey+ e22 <- toText <$> mkE2Biscuit 2 appSecretKey+ ttld <- toText <$> (addTtl later =<< mkAdminBiscuit appSecretKey)+ expd <- toText <$> (addTtl earlier =<< mkAdminBiscuit appSecretKey) print adminB hspec $ around (withApp $ app appPk) $@@ -41,21 +45,28 @@ runC port (call2 e21 1) `shouldReturn` Right 2 runC port (call2 e22 1) `shouldReturn` Left (Just "Biscuit failed checks") runC port (call3 anonB) `shouldReturn` Left (Just "Biscuit failed checks")+ it "Effectful verification should work as expected" $ \port -> do+ runC port (call1 ttld) `shouldReturn` Right 1+ runC port (call1 expd) `shouldReturn` Left (Just "Biscuit failed checks") -appPrivateKey :: PrivateKey-appPrivateKey = fromJust . parsePrivateKeyHex $ "c2b7507af4f849fd028d0f7e90b04a4e74d9727b358fca18b65beffd86c47209"+appSecretKey :: SecretKey+appSecretKey = fromJust . parseSecretKeyHex $ "c2b7507af4f849fd028d0f7e90b04a4e74d9727b358fca18b65beffd86c47209" -toText :: Biscuit -> Text+toText :: BiscuitProof p => Biscuit p Verified -> Text toText = decodeUtf8 . serializeB64 -mkAdminBiscuit :: Keypair -> IO Biscuit-mkAdminBiscuit kp = mkBiscuit kp [block|right(#authority, #admin);|]+mkAdminBiscuit :: SecretKey -> IO (Biscuit Open Verified)+mkAdminBiscuit sk = mkBiscuit sk [block|right("admin");|] -mkAnonBiscuit :: Keypair -> IO Biscuit-mkAnonBiscuit kp = mkBiscuit kp [block|right(#authority, #anon);|]+mkAnonBiscuit :: SecretKey -> IO (Biscuit Open Verified)+mkAnonBiscuit sk = mkBiscuit sk [block|right("anon");|] -mkE1Biscuit :: Keypair -> IO Biscuit-mkE1Biscuit kp = mkBiscuit kp [block|right(#authority, #one);|]+mkE1Biscuit :: SecretKey -> IO (Biscuit Open Verified)+mkE1Biscuit sk = mkBiscuit sk [block|right("one");|] -mkE2Biscuit :: Int -> Keypair -> IO Biscuit-mkE2Biscuit v kp = mkBiscuit kp [block|right(#authority, #two, ${v});|]+mkE2Biscuit :: Int -> SecretKey -> IO (Biscuit Open Verified)+mkE2Biscuit v sk = mkBiscuit sk [block|right("two", ${v});|]++addTtl :: UTCTime -> Biscuit Open Verified -> IO (Biscuit Open Verified)+addTtl expiration =+ addBlock [block|check if time($now), $now < ${expiration};|]