packages feed

polysemy-account-api-0.2.0.0: lib/Polysemy/Account/Api/Server/AuthEndpoint.hs

{-# options_haddock prune #-}

-- | Combinators for guarding Servant handlers with authentication and authorization
module Polysemy.Account.Api.Server.AuthEndpoint where

import Exon (exon)
import qualified Log
import Servant (ServerError)
import Servant.Auth.Server (AuthResult (Authenticated))

import Polysemy.Account.Api.Effect.Authorize (Authorize, authorize)
import qualified Polysemy.Account.Data.AccountStatus as AccountStatus
import Polysemy.Account.Data.AccountsError (AccountsError (Client, Internal))
import Polysemy.Account.Data.AuthedAccount (AuthedAccount (AuthedAccount))
import qualified Polysemy.Account.Data.Privilege as Privilege
import Polysemy.Account.Data.Privilege (Privilege)
import qualified Polysemy.Account.Effect.Accounts as Accounts
import Polysemy.Account.Effect.Accounts (Accounts)
import Polysemy.Account.Api.Server.Error (unauthorized, internal)

-- | Basic values for describing the requirements of an endpoint for either "any user" or "admin".
class AuthEndpointParam param where
  authEndpointUser :: param
  authEndpointAdmin :: param

instance AuthEndpointParam [Privilege] where
  authEndpointUser = []
  authEndpointAdmin = [Privilege.Admin]

pattern Active :: AuthedAccount i p -> AuthedAccount i p
pattern Active acc <- acc@(AuthedAccount _ _ _ AccountStatus.Active _)

insufficient ::
  ∀ i p r a .
  Show (AuthedAccount i p) =>
  Members [Log, Stop ServerError] r =>
  AuthedAccount i p ->
  Text ->
  Sem r a
insufficient account extra = do
  Log.error [exon|auth with insufficient privileges: #{show account}#{extra}|]
  stop (unauthorized "insufficient privileges")

checkAccount ::
  ∀ i param p r a .
  Show (AuthedAccount i p) =>
  Members [Authorize i param p, Log, Stop ServerError] r =>
  param ->
  (AuthedAccount i p -> Sem r a) ->
  AuthedAccount i p ->
  Sem r a
checkAccount param f (Active account) =
  authorize param account >>= \case
    Just msg ->
      insufficient account [exon| (#{msg})|]
    Nothing ->
      f account
checkAccount _ _ account =
  insufficient account ""

type AuthEndpoint i param p r =
  (Members [Authorize i param p, Accounts i p !! AccountsError, Log, Stop ServerError] r, AuthEndpointParam param)

-- | Wrap an authenticated handler function with an authorization check.
--
-- Sends the account information and given endpoint param to the 'Authorize' effect if the authentication material is
-- valid.
authorizeEndpoint ::
  ∀ i param p r a .
  Show (AuthedAccount i p) =>
  AuthEndpoint i param p r =>
  param ->
  (AuthedAccount i p -> Sem r a) ->
  AuthResult (AuthedAccount i p) ->
  Sem r a
authorizeEndpoint param f (Authenticated payload@(AuthedAccount _ authId _ _ _)) = do
  account <- Accounts.authed authId !! \case
    Client _ -> insufficient payload " (token expired)"
    Internal _ -> stop (internal "Fatal error")
  checkAccount param f account
authorizeEndpoint _ _ _ =
  stop (unauthorized "no valid auth data")

-- | Require that the authentication material belongs to an active account.
accountOnly ::
  ∀ i param p r a .
  Show (AuthedAccount i p) =>
  AuthEndpoint i param p r =>
  (AuthedAccount i p -> Sem r a) ->
  AuthResult (AuthedAccount i p) ->
  Sem r a
accountOnly =
  authorizeEndpoint authEndpointUser

-- | Require that the authentication material belongs to an active account.
accountOnly_ ::
  ∀ i param p r a .
  Show (AuthedAccount i p) =>
  AuthEndpoint i param p r =>
  Sem r a ->
  AuthResult (AuthedAccount i p) ->
  Sem r a
accountOnly_ f =
  accountOnly (const f)

-- | Require that the authentication material belongs to an active account.
accountOnly1 ::
  ∀ i param p r a b .
  Show (AuthedAccount i p) =>
  AuthEndpoint i param p r =>
  (AuthedAccount i p -> a -> Sem r b) ->
  AuthResult (AuthedAccount i p) ->
  a ->
  Sem r b
accountOnly1 f authResult a =
  accountOnly (\ acc -> f acc a) authResult

-- | Require that the authentication material belongs to an active account.
accountOnly1_ ::
  ∀ i param p r a b .
  Show (AuthedAccount i p) =>
  AuthEndpoint i param p r =>
  (a -> Sem r b) ->
  AuthResult (AuthedAccount i p) ->
  a ->
  Sem r b
accountOnly1_ f authResult a =
  accountOnly_ (f a) authResult

-- | Require that the authentication material belongs to an active account.
accountOnly2 ::
  ∀ i param p r a b c .
  Show (AuthedAccount i p) =>
  AuthEndpoint i param p r =>
  (AuthedAccount i p -> a -> b -> Sem r c) ->
  AuthResult (AuthedAccount i p) ->
  a ->
  b ->
  Sem r c
accountOnly2 f authResult a b =
  accountOnly (\ acc -> f acc a b) authResult

-- | Require that the authentication material belongs to an active account.
accountOnly2_ ::
  ∀ i param p r a b c .
  Show (AuthedAccount i p) =>
  AuthEndpoint i param p r =>
  (a -> b -> Sem r c) ->
  AuthResult (AuthedAccount i p) ->
  a ->
  b ->
  Sem r c
accountOnly2_ f authResult a b =
  accountOnly_ (f a b) authResult

-- | Require that the authentication material belongs to an active admin account.
adminOnly ::
  ∀ i param p r a .
  Show (AuthedAccount i p) =>
  AuthEndpoint i param p r =>
  (AuthedAccount i p -> Sem r a) ->
  AuthResult (AuthedAccount i p) ->
  Sem r a
adminOnly =
  authorizeEndpoint authEndpointAdmin

-- | Require that the authentication material belongs to an active admin account.
adminOnly_ ::
  ∀ i param p r a .
  Show (AuthedAccount i p) =>
  AuthEndpoint i param p r =>
  Sem r a ->
  AuthResult (AuthedAccount i p) ->
  Sem r a
adminOnly_ f =
  adminOnly (const f)

-- | Require that the authentication material belongs to an active admin account.
adminOnly1 ::
  ∀ i param p r a b .
  Show (AuthedAccount i p) =>
  AuthEndpoint i param p r =>
  (AuthedAccount i p -> a -> Sem r b) ->
  AuthResult (AuthedAccount i p) ->
  a ->
  Sem r b
adminOnly1 f authResult a =
  adminOnly (\ acc -> f acc a) authResult

-- | Require that the authentication material belongs to an active admin account.
adminOnly1_ ::
  ∀ i param p r a b .
  Show (AuthedAccount i p) =>
  AuthEndpoint i param p r =>
  (a -> Sem r b) ->
  AuthResult (AuthedAccount i p) ->
  a ->
  Sem r b
adminOnly1_ f authResult a =
  adminOnly_ (f a) authResult

-- | Require that the authentication material belongs to an active admin account.
adminOnly2 ::
  ∀ i param p r a b c .
  Show (AuthedAccount i p) =>
  AuthEndpoint i param p r =>
  (AuthedAccount i p -> a -> b -> Sem r c) ->
  AuthResult (AuthedAccount i p) ->
  a ->
  b ->
  Sem r c
adminOnly2 f authResult a b =
  adminOnly (\ acc -> f acc a b) authResult

-- | Require that the authentication material belongs to an active admin account.
adminOnly2_ ::
  ∀ i param p r a b c .
  Show (AuthedAccount i p) =>
  AuthEndpoint i param p r =>
  (a -> b -> Sem r c) ->
  AuthResult (AuthedAccount i p) ->
  a ->
  b ->
  Sem r c
adminOnly2_ f authResult a b =
  adminOnly_ (f a b) authResult