{-# LANGUAGE UndecidableInstances, InstanceSigs #-}
{-# OPTIONS_GHC -fno-warn-redundant-constraints -fprof-auto #-}

module Foundation.Authorization
  ( evalAccess, evalAccessFor, evalAccessWith
  , evalAccessDB, evalAccessForDB, evalAccessWithDB
  , hasReadAccessTo, hasWriteAccessTo
  , wouldHaveReadAccessTo, wouldHaveWriteAccessTo
  , wouldHaveReadAccessToIff, wouldHaveWriteAccessToIff
  , AuthContext(..), getAuthContext
  , isDryRun, isDryRunDB
  , IsDryRun(..)
  , maybeBearerToken, requireBearerToken
  , requireCurrentBearerRestrictions, maybeCurrentBearerRestrictions
  , BearerAuthSite, MonadAP
  , routeAuthTags
  , orAR, andAR, notAR, trueAR, falseAR
  , authoritiveApproot
  , AuthorizationCacheKey(..)
  ) where

import Import.NoFoundation hiding (Last(..))

import Foundation.Type
import Foundation.Routes
import Foundation.I18n

import Foundation.DB

import Handler.Utils.ExamOffice.Exam
import Handler.Utils.ExamOffice.ExternalExam
import Handler.Utils.Memcached
import Handler.Utils.I18n
import Handler.Utils.Routes
import Utils.Course (courseIsVisible)
import Utils.Metrics (observeAuthTagEvaluation, AuthTagEvalOutcome(..))
  
import qualified Data.Set as Set
import qualified Data.Aeson as JSON
import qualified Data.HashSet as HashSet
import qualified Data.Map as Map
import Data.Map ((!?))
import qualified Data.Text as Text
import Data.List (findIndex)
-- import Data.Semigroup (Last(..))

import qualified Database.Esqueleto.Legacy as E
import qualified Database.Esqueleto.Utils as E

import Control.Monad.Error.Class (MonadError(..))
import Control.Monad.Memo.Class (MonadMemo(..), for4)

import Data.Aeson.Lens hiding (_Value, key)

-- import qualified Data.Conduit.Combinators as C

import qualified Data.Binary as Binary

import GHC.TypeLits (TypeError)
import qualified GHC.TypeLits as TypeError (ErrorMessage(..))

-- import Utils.VolatileClusterSettings


type BearerAuthSite site
  = ( MonadCrypto (HandlerFor site)
    , CryptoIDKey ~ MonadCryptoKey (HandlerFor site)
    , MonadCrypto (ReaderT SqlBackend (HandlerFor site))
    , CryptoIDKey ~ MonadCryptoKey (ReaderT SqlBackend (HandlerFor site))
    , MonadCrypto (ExceptT AuthResult (HandlerFor site))
    , CryptoIDKey ~ MonadCryptoKey (ExceptT AuthResult (HandlerFor site))
    , MonadCrypto (MaybeT (HandlerFor site))
    , CryptoIDKey ~ MonadCryptoKey (MaybeT (HandlerFor site))
    , MonadCrypto (ExceptT AuthResult (ReaderT SqlReadBackend (HandlerFor site)))
    , CryptoIDKey ~ MonadCryptoKey (ExceptT AuthResult (ReaderT SqlReadBackend (HandlerFor site)))
    , MonadCrypto (ReaderT SqlReadBackend (HandlerFor site))
    , CryptoIDKey ~ MonadCryptoKey (ReaderT SqlReadBackend (HandlerFor site))
    , MonadCrypto (MaybeT (WriterT Any (ReaderT SqlReadBackend (HandlerFor site))))
    , CryptoIDKey ~ MonadCryptoKey (MaybeT (WriterT Any (ReaderT SqlReadBackend (HandlerFor site))))
    , MonadCrypto (MaybeT (ReaderT SqlReadBackend (HandlerFor site)))
    , CryptoIDKey ~ MonadCryptoKey (MaybeT (ReaderT SqlReadBackend (HandlerFor site)))
    , UserId ~ AuthId site, User ~ AuthEntity site
    , YesodAuthPersist site
    )


-- Access Control
newtype InvalidAuthTag = InvalidAuthTag Text
  deriving (Eq, Ord, Show, Read, Generic, Typeable)
instance Exception InvalidAuthTag


type AuthTagsEval m = AuthDNF -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> WriterT (Set AuthTag) m AuthResult
  
data AccessPredicate
  = APPure     (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> Reader MsgRenderer AuthResult)
  | APHandler  (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> HandlerFor UniWorX AuthResult)
  | APDB       (ByteString -> (forall m. MonadAP m => AuthTagsEval m) -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> ReaderT SqlReadBackend (HandlerFor UniWorX) AuthResult)
  | APBind     (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> HandlerFor UniWorX (Either AccessPredicate AuthResult))
  | APBindDB   (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> HandlerFor UniWorX (Either (ReaderT SqlReadBackend (HandlerFor UniWorX) (Either AccessPredicate AuthResult)) (Either AccessPredicate AuthResult)))

class (MonadHandler m, HandlerSite m ~ UniWorX, BearerAuthSite UniWorX, MonadUnliftIO m) => MonadAP m where
  evalAccessPred :: AccessPredicate -> ByteString -> (forall m'. MonadAP m' => AuthTagsEval m') -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m AuthResult

  apRunDB :: forall a. ReaderT SqlReadBackend (HandlerFor UniWorX) a -> m a

type family DisabledMonadAPInstance t err :: Constraint where
  DisabledMonadAPInstance t err
    = TypeError (                 'TypeError.Text "Used dangerous MonadAP instance for: " 'TypeError.:<>: 'TypeError.ShowType t
                  'TypeError.:$$: 'TypeError.Text "This instance is currently disabled via TypeError because: " 'TypeError.:<>: err
                  'TypeError.:$$: 'TypeError.Text "Please consider removing the usage triggering this error message before re-enabling or removing the instance."
                )

instance ( BearerAuthSite UniWorX
         -- , DisabledMonadAPInstance (HandlerFor UniWorX) ('TypeError.Text "It causes too many database connections")
         ) => MonadAP (HandlerFor UniWorX) where
  evalAccessPred :: HasCallStack => AccessPredicate -> ByteString -> (forall m'. MonadAP m' => AuthTagsEval m') -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> HandlerFor UniWorX AuthResult
  evalAccessPred aPred contCtx cont aid r w = case aPred of
    (APPure    p) -> runReader (p aid r w) <$> getMsgRenderer
    (APHandler p) -> p aid r w
    (APDB      p) -> apRunDB $ p contCtx cont aid r w
    (APBind    p) -> evalAccessPred (APBindDB $ \aid' r' w' -> Right <$> p aid' r' w') contCtx cont aid r w
    (APBindDB  p) -> let contAP p' = evalAccessPred p' contCtx cont aid r w
                      in p aid r w >>= either apRunDB return >>= either contAP return

  apRunDB :: forall a. HasCallStack => ReaderT SqlReadBackend (HandlerFor UniWorX) a -> HandlerFor UniWorX a
  apRunDB = runDBRead' callStack

instance BearerAuthSite UniWorX => MonadAP (WidgetFor UniWorX) where
  evalAccessPred :: HasCallStack => AccessPredicate -> ByteString -> (forall m'. MonadAP m' => AuthTagsEval m') -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> WidgetFor UniWorX AuthResult
  evalAccessPred aPred contCtx cont aid r w = liftHandler $ evalAccessPred aPred contCtx cont aid r w

  apRunDB :: forall a. HasCallStack => ReaderT SqlReadBackend (HandlerFor UniWorX) a -> WidgetFor UniWorX a
  apRunDB = liftHandler . apRunDB

instance (MonadHandler m, HandlerSite m ~ UniWorX, BackendCompatible SqlReadBackend backend, BearerAuthSite UniWorX, MonadUnliftIO m) => MonadAP (ReaderT backend m) where
  evalAccessPred aPred contCtx cont aid r w = mapReaderT liftHandler . withReaderT (projectBackend @SqlReadBackend) $ case aPred of
    (APPure    p) -> lift $ runReader (p aid r w) <$> getMsgRenderer
    (APHandler p) -> lift $ p aid r w
    (APDB      p) -> p contCtx cont aid r w
    (APBind    p) -> evalAccessPred (APBindDB $ \aid' r' w' -> Right <$> p aid' r' w') contCtx cont aid r w
    (APBindDB  p) -> let contAP p' = evalAccessPred p' contCtx cont aid r w
                      in lift (p aid r w) >>= either id return >>= either contAP return

  apRunDB = hoist liftHandler . withReaderT projectBackend

-- cacheAP :: ( Binary k
--            , Typeable v, Binary v
--            )
--         => Maybe Expiry
--         -> k
--         -> HandlerFor UniWorX v
--         -> (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> v -> Either AccessPredicate (HandlerFor UniWorX AuthResult))
--         -> AccessPredicate
-- cacheAP mExp k mkV cont = APBind $ \mAuthId route isWrite -> either (return . Left) (fmap Right) . cont mAuthId route isWrite =<< memcachedBy mExp k mkV

cacheAPDB :: ( Binary k
             , Typeable v, Binary v, NFData v
             )
          => Maybe Expiry
          -> k
          -> ReaderT SqlReadBackend (HandlerFor UniWorX) v
          -> (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> v -> Either AccessPredicate (HandlerFor UniWorX AuthResult))
          -> AccessPredicate
cacheAPDB mExp k mkV cont = APBindDB $ \mAuthId route isWrite -> do
  cachedV <- memcachedByGet k
  case cachedV of
    Just v -> fmap Right . either (return . Left) (fmap Right) $ cont mAuthId route isWrite v
    Nothing -> return . Left $ do
      v <- mkV
      memcachedBySet mExp k v
      either (return . Left) (fmap Right . lift) $ cont mAuthId route isWrite v
 
-- cacheAP' :: ( Binary k
--             , Typeable v, Binary v
--             )
--          => Maybe Expiry
--          -> (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> Maybe (k, HandlerFor UniWorX v))
--          -> (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> Maybe v -> Either AccessPredicate (HandlerFor UniWorX AuthResult))
--          -> AccessPredicate
-- cacheAP' mExp mkKV cont = APBind $ \mAuthId route isWrite -> case mkKV mAuthId route isWrite of
--   Just (k, mkV) -> either (return . Left) (fmap Right) . cont mAuthId route isWrite . Just =<< memcachedBy mExp k mkV
--   Nothing -> either (return . Left) (fmap Right) $ cont mAuthId route isWrite Nothing
  
cacheAPDB' :: ( Binary k
              , Typeable v, Binary v, NFData v
              )
           => Maybe Expiry
           -> (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> Maybe (k, ReaderT SqlReadBackend (HandlerFor UniWorX) v))
           -> (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> Maybe v -> Either AccessPredicate (HandlerFor UniWorX AuthResult))
           -> AccessPredicate
cacheAPDB' mExp mkKV cont = APBindDB $ \mAuthId route isWrite -> case mkKV mAuthId route isWrite of
  Just (k, mkV) -> do
    cachedV <- memcachedByGet k
    case cachedV of
      Just v -> fmap Right . either (return . Left) (fmap Right) . cont mAuthId route isWrite $ Just v
      Nothing -> return . Left $ do
        v <- mkV
        memcachedBySet mExp k v
        either (return . Left) (fmap Right . lift) . cont mAuthId route isWrite $ Just v
  Nothing -> fmap Right . either (return . Left) (fmap Right) $ cont mAuthId route isWrite Nothing


orAR, andAR :: MsgRenderer -> AuthResult -> AuthResult -> AuthResult
orAR  _  Authorized _ = Authorized
orAR  _  _ Authorized = Authorized
orAR  _  AuthenticationRequired _  = AuthenticationRequired
orAR  _  _ AuthenticationRequired  = AuthenticationRequired
orAR  mr (Unauthorized x) (Unauthorized y) = Unauthorized . render mr $ MsgUnauthorizedOr x y
-- and
andAR mr (Unauthorized x) (Unauthorized y) = Unauthorized . render mr $ MsgUnauthorizedAnd x y
andAR _  reason@(Unauthorized _) _ = reason
andAR _  _ reason@(Unauthorized _) = reason
andAR _  Authorized          other = other
andAR _  AuthenticationRequired  _ = AuthenticationRequired

_orARI18n, _andARI18n :: MsgRenderer -> I18nAuthResult -> I18nAuthResult -> I18nAuthResult
_orARI18n _  AuthorizedI18n _ = AuthorizedI18n
_orARI18n _  _ AuthorizedI18n = AuthorizedI18n
_orARI18n _  AuthenticationRequiredI18n _  = AuthenticationRequiredI18n
_orARI18n _  _ AuthenticationRequiredI18n  = AuthenticationRequiredI18n
_orARI18n mr (UnauthorizedI18n x) (UnauthorizedI18n y) = fmap (Unauthorized . render mr) . MsgUnauthorizedOr <$> x <*> y
_orARI18n mr _ _ = UnauthorizedI18n . opoint $ render mr MsgUnauthorizedI18nMismatch
-- and
_andARI18n mr (UnauthorizedI18n x) (UnauthorizedI18n y) = fmap (Unauthorized . render mr) . MsgUnauthorizedAnd <$> x <*> y
_andARI18n _ reason@(UnauthorizedI18n _) _ = reason
_andARI18n _ _ reason@(UnauthorizedI18n _) = reason
_andARI18n _ AuthorizedI18n other = other
_andARI18n _  AuthenticationRequiredI18n  _ = AuthenticationRequiredI18n
_andARI18n mr _ _ = UnauthorizedI18n . opoint $ render mr MsgUnauthorizedI18nMismatch

notAR :: RenderMessage UniWorX msg => MsgRenderer -> msg -> AuthResult -> AuthResult
notAR _  _   (Unauthorized _)        = Authorized
notAR _  _    AuthenticationRequired = AuthenticationRequired
notAR mr msg  Authorized             = Unauthorized . render mr . MsgUnauthorizedNot $ render mr msg

trueAR, falseAR :: MsgRendererS UniWorX -> AuthResult
trueAR = const Authorized
falseAR = Unauthorized . ($ MsgUnauthorized) . render

trueAP, _falseAP :: AccessPredicate
trueAP  = APPure . const . const . const $ trueAR <$> ask
_falseAP = APPure . const . const . const $ falseAR <$> ask -- included for completeness


data AuthContext = AuthContext
  { authCtxAuth    :: Maybe (AuthId UniWorX)
  , authCtxBearer  :: Maybe (BearerToken UniWorX)
  , authActiveTags :: AuthTagActive
  } deriving (Generic, Typeable)

deriving stock instance Eq (AuthId UniWorX) => Eq AuthContext
deriving stock instance Ord (AuthId UniWorX) => Ord AuthContext
deriving stock instance (Read (AuthId UniWorX), Eq (AuthId UniWorX), Hashable (AuthId UniWorX)) => Read AuthContext
deriving stock instance (Show (AuthId UniWorX), Eq (AuthId UniWorX), Hashable (AuthId UniWorX)) => Show AuthContext
deriving anyclass instance Hashable (AuthId UniWorX) => Hashable AuthContext
deriving anyclass instance (Binary (AuthId UniWorX), Eq (AuthId UniWorX), Hashable (AuthId UniWorX)) => Binary AuthContext

getAuthContext :: forall m.
                  ( MonadHandler m, HandlerSite m ~ UniWorX
                  , BearerAuthSite UniWorX
                  )
               => m AuthContext
getAuthContext = liftHandler $ do
  authCtx <- AuthContext
    <$> defaultMaybeAuthId
    <*> runMaybeT (exceptTMaybe askBearerUnsafe)
    <*> (fromMaybe def <$> lookupSessionJson SessionActiveAuthTags)

  $logDebugS "getAuthContext" $ tshow authCtx

  return authCtx

newtype IsDryRun = MkIsDryRun { unIsDryRun :: Bool }
  deriving stock (Read, Show, Generic, Typeable)
  deriving newtype (Eq, Ord)
  deriving (Semigroup, Monoid) via Any

isDryRun :: ( HasCallStack
            , BearerAuthSite UniWorX
            )
         => HandlerFor UniWorX Bool
isDryRun = fmap unIsDryRun . cached . fmap MkIsDryRun $ runDBRead isDryRunDB

isDryRunDB :: forall m backend m'.
              ( HasCallStack
              , MonadAP m, MonadCatch m
              , MonadSite UniWorX m'
              , BearerAuthSite UniWorX
              , WithRunDB backend m' m
              , BackendCompatible SqlReadBackend backend
              )
           => m Bool
isDryRunDB = fmap unIsDryRun . cached . fmap MkIsDryRun $ orM
  [ hasGlobalPostParam PostDryRun
  , hasGlobalGetParam GetDryRun
  , hasCustomHeader HeaderDryRun
  , and2M bearerDryRun bearerRequired
  ]
  where
    bearerDryRun = has (_Just . _Object . ix "dry-run") <$> maybeCurrentBearerRestrictions @Value
    bearerRequired = maybeT (return True) . catchIfMaybeT cPred $ do
      mAuthId <- liftHandler defaultMaybeAuthId
      currentRoute <- liftHandler $ maybe (permissionDeniedI MsgUnauthorizedToken404) return =<< getCurrentRoute
      isWrite <- liftHandler $ isWriteRequest currentRoute

      let noTokenAuth :: AuthDNF -> AuthDNF
          noTokenAuth = over _dnfTerms . Set.filter . noneOf (re _nullable . folded) $ (== AuthToken) . plVar

      dnf <- throwLeft $ routeAuthTags currentRoute
      let eval :: forall m''. MonadAP m'' => AuthTagsEval m''
          eval dnf' mAuthId' route' isWrite' = evalAuthTags 'isDryRun (AuthTagActive $ const True) eval (noTokenAuth dnf') mAuthId' route' isWrite'
       in guardAuthResult <=< evalWriterT $ eval dnf mAuthId currentRoute isWrite

      return False

    cPred err = any ($ err)
      [ is $ _HCError . _PermissionDenied
      , is $ _HCError . _NotAuthenticated
      ]


askBearerUnsafe :: forall m.
                  ( MonadHandler m, HandlerSite m ~ UniWorX
                  , BearerAuthSite UniWorX
                  )
               => ExceptT AuthResult m (BearerToken UniWorX)
-- | This performs /no/ meaningful validation of the `BearerToken`
--
-- Use `requireBearerToken` or `maybeBearerToken` instead
askBearerUnsafe = ExceptT . $cachedHere . liftHandler . runExceptT $ do
  bearer <- maybeMExceptT (unauthorizedI MsgUnauthorizedNoToken) askBearer
  catch (decodeBearer bearer) $ \case
    BearerTokenExpired -> throwError =<< unauthorizedI MsgUnauthorizedTokenExpired
    BearerTokenNotStarted -> throwError =<< unauthorizedI MsgUnauthorizedTokenNotStarted
    other -> do
      $logWarnS "AuthToken" $ tshow other
      throwError =<< unauthorizedI MsgUnauthorizedTokenInvalid

validateBearer :: forall m.
                  ( HasCallStack
                  , MonadHandler m, HandlerSite m ~ UniWorX
                  , MonadCatch m, MonadAP m
                  , BearerAuthSite UniWorX
                  )
               => Maybe (AuthId UniWorX)
               -> Route UniWorX
               -> Bool -- ^ @isWrite@
               -> BearerToken UniWorX
               -> m AuthResult
validateBearer mAuthId' route' isWrite' token' = $runCachedMemoT $ for4 memo validateBearer' mAuthId' route' isWrite' token'
  where
    validateBearer' :: _ -> _ -> _ -> _ -> CachedMemoT (Maybe (AuthId UniWorX), Route UniWorX, Bool, BearerToken UniWorX) AuthResult m AuthResult
    validateBearer' mAuthId route isWrite BearerToken{..} = lift . exceptT return return $ do
      iforM_ bearerRoutes $ \case
        BearerTokenRouteEval -> \routes -> guardMExceptT (HashSet.member route routes) $ unauthorizedI MsgUnauthorizedTokenInvalidRoute
        BearerTokenRouteAccess -> \routes -> maybeTMExceptT (unauthorizedI MsgUnauthorizedTokenInvalidRoute) $ do
          cRoute <- MaybeT getCurrentRoute
          guard $ HashSet.member cRoute routes

      let
        -- Prevent infinite loops
        noTokenAuth :: AuthDNF -> AuthDNF
        noTokenAuth = over _dnfTerms . Set.filter . noneOf (re _nullable . folded) $ (== AuthToken) . plVar

        eval :: forall m'. MonadAP m' => AuthTagsEval m'
        eval dnf' mAuthId'' route'' isWrite'' = evalAuthTags 'validateBearer (AuthTagActive $ const True) eval (noTokenAuth dnf') mAuthId'' route'' isWrite''

      bearerAuthority' <- hoist apRunDB $ do
        bearerAuthority' <- flip foldMapM bearerAuthority $ \case
          Left tVal
            | JSON.Success groupName <- JSON.fromJSON tVal -> do
                Entity _ primary <- maybeMExceptT (unauthorizedI MsgUnauthorizedTokenInvalidAuthorityGroup) . getBy $ UniquePrimaryUserGroupMember groupName Active
                case bearerImpersonate of
                  Nothing -> return . Set.singleton $ userGroupMemberUser primary
                  Just iuid | iuid == userGroupMemberUser primary -> return . Set.singleton $ userGroupMemberUser primary
                            | otherwise -> do
                                unlessM (lift $ exists [UserGroupMemberUser ==. iuid, UserGroupMemberGroup ==. groupName]) $
                                  throwError =<< unauthorizedI MsgUnauthorizedTokenInvalidImpersonation
                                return $ Set.singleton iuid
            | otherwise -> throwError =<< unauthorizedI MsgUnauthorizedTokenInvalidAuthorityValue
          Right uid -> case bearerImpersonate of
            Just iuid | uid == iuid -> return $ Set.singleton uid
                      | otherwise -> do
                          cID <- encrypt iuid
                          unlessM (lift $ is _Authorized <$> evalAccessWithFor [(AuthToken, False)] (Just uid) (AdminHijackUserR cID) True) $
                            throwError =<< unauthorizedI MsgUnauthorizedTokenInvalidImpersonation
                          return $ Set.singleton iuid
            Nothing -> return $ Set.singleton uid

        guardMExceptT (not $ Set.null bearerAuthority') $ unauthorizedI MsgUnauthorizedTokenInvalidNoAuthority

        forM_ bearerAuthority' $ \uid -> do
          User{userTokensIssuedAfter} <- maybeMExceptT (unauthorizedI MsgUnauthorizedTokenInvalidAuthority) $ get uid
          guardMExceptT (Just bearerIssuedAt >= userTokensIssuedAfter) (unauthorizedI MsgUnauthorizedTokenExpired)

        return bearerAuthority'

      forM_ bearerAuthority' $ \uid -> do
        authorityVal <- do
          dnf <- throwLeft $ routeAuthTags route
          lift . evalWriterT $ eval (noTokenAuth dnf) (Just uid) route isWrite
        guardExceptT (is _Authorized authorityVal) authorityVal

      whenIsJust bearerAddAuth $ \addDNF -> do
        $logDebugS "validateToken" $ tshow addDNF
        additionalVal <- lift . evalWriterT $ eval (noTokenAuth addDNF) mAuthId route isWrite
        guardExceptT (is _Authorized additionalVal) additionalVal

      return Authorized

maybeBearerToken :: ( HasCallStack
                    , MonadHandler m, HandlerSite m ~ UniWorX
                    , BearerAuthSite UniWorX
                    , MonadAP m
                    , MonadCatch m
                    ) => m (Maybe (BearerToken UniWorX))
maybeBearerToken = $cachedHere . runMaybeT $ catchIfMaybeT cPred requireBearerToken
  where
    cPred err = any ($ err)
      [ is $ _HCError . _PermissionDenied
      , is $ _HCError . _NotAuthenticated
      ]

requireBearerToken :: forall m.
                      ( HasCallStack
                      , MonadHandler m, HandlerSite m ~ UniWorX
                      , BearerAuthSite UniWorX
                      , MonadAP m
                      , MonadCatch m
                      )
                   => m (BearerToken UniWorX)
requireBearerToken = do
  bearer <- exceptT (guardAuthResult >=> error "askToken should not throw `Authorized`") return askBearerUnsafe
  mAuthId <- defaultMaybeAuthId -- `maybeAuthId` would be an infinite loop; this is equivalent to `maybeAuthId` but ignoring `bearerImpersonate` from any valid token
  currentRoute <- maybe (permissionDeniedI MsgUnauthorizedToken404) return =<< getCurrentRoute
  isWrite <- liftHandler $ isWriteRequest currentRoute
  guardAuthResult =<< validateBearer mAuthId currentRoute isWrite bearer
  return bearer

requireCurrentBearerRestrictions :: forall a m.
                                    ( HasCallStack
                                    , MonadHandler m, HandlerSite m ~ UniWorX
                                    , FromJSON a, ToJSON a
                                    , BearerAuthSite UniWorX
                                    , MonadAP m
                                    , MonadCatch m
                                    )
                                 => m (Maybe a)
requireCurrentBearerRestrictions = runMaybeT $ do
  bearer <- lift requireBearerToken
  route <- MaybeT getCurrentRoute
  hoistMaybe $ bearer ^? _bearerRestrictionIx route

maybeCurrentBearerRestrictions :: forall a m.
                                  ( HasCallStack
                                  , MonadHandler m, HandlerSite m ~ UniWorX
                                  , FromJSON a, ToJSON a
                                  , BearerAuthSite UniWorX
                                  , MonadAP m
                                  , MonadCatch m
                                  )
                               => m (Maybe a)
maybeCurrentBearerRestrictions = runMaybeT $ do
  bearer <- MaybeT maybeBearerToken
  route <- MaybeT getCurrentRoute
  hoistMaybe $ bearer ^? _bearerRestrictionIx route

data AuthorizationCacheKey
  = AuthCacheSchoolFunctionList SchoolFunction | AuthCacheSystemFunctionList SystemFunction
  | AuthCacheLecturerList | AuthCacheExternalExamStaffList | AuthCacheCorrectorList | AuthCacheExamCorrectorList | AuthCacheTutorList | AuthCacheSubmissionGroupUserList
  | AuthCacheCourseRegisteredList TermId SchoolId CourseShorthand
  | AuthCacheVisibleSystemMessages
  deriving (Eq, Ord, Read, Show, Generic, Typeable)
  deriving anyclass (Hashable, Binary)

cacheAPSchoolFunction :: BearerAuthSite UniWorX
                      => SchoolFunction
                      -> Maybe Expiry
                      -> (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> Set (AuthId UniWorX) -> Either AccessPredicate (HandlerFor UniWorX AuthResult))
                      -> AccessPredicate
cacheAPSchoolFunction f mExp = cacheAPDB mExp (AuthCacheSchoolFunctionList f) mkFunctionList
  where
    mkFunctionList = fmap (setOf $ folded . _Value) . E.select . E.from $ \userFunction -> do
      E.where_ $ userFunction E.^. UserFunctionFunction E.==. E.val f
      return $ userFunction E.^. UserFunctionUser

cacheAPSystemFunction :: BearerAuthSite UniWorX
                      => SystemFunction
                      -> Maybe Expiry
                      -> (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> Set (AuthId UniWorX) -> Either AccessPredicate (HandlerFor UniWorX AuthResult))
                      -> AccessPredicate
cacheAPSystemFunction f mExp = cacheAPDB mExp (AuthCacheSystemFunctionList f) mkFunctionList
  where
    mkFunctionList = fmap (setOf $ folded . _Value) . E.select . E.from $ \userSystemFunction -> do
      E.where_ $ userSystemFunction E.^. UserSystemFunctionFunction E.==. E.val f
           E.&&. E.not_ (userSystemFunction E.^. UserSystemFunctionIsOptOut)
      return $ userSystemFunction E.^. UserSystemFunctionUser

tagAccessPredicate :: ( HasCallStack
                      , BearerAuthSite UniWorX
                      )
                   => AuthTag -> AccessPredicate
tagAccessPredicate AuthFree = trueAP
tagAccessPredicate AuthAdmin = cacheAPSchoolFunction SchoolAdmin (Just $ Right diffHour) $ \mAuthId' route' _ adminList -> if
  | maybe True (`Set.notMember` adminList) mAuthId' -> Right $ case route' of
      _ | is _Nothing mAuthId' -> return AuthenticationRequired
      CourseR{} -> unauthorizedI MsgUnauthorizedSchoolAdmin
      AllocationR{} -> unauthorizedI MsgUnauthorizedSchoolAdmin
      SchoolR _ _ -> unauthorizedI MsgUnauthorizedSchoolAdmin
      _other -> unauthorizedI MsgUnauthorizedSiteAdmin
  | otherwise -> Left $ APDB $ \_ _ mAuthId route _ -> case route of
      -- Courses: access only to school admins
      CourseR tid ssh csh _ -> $cachedHereBinary (mAuthId, tid, ssh, csh) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isAdmin <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` userAdmin) -> do
          E.on $ course E.^. CourseSchool E.==. userAdmin E.^. UserFunctionSchool
          E.where_  $ userAdmin E.^. UserFunctionUser E.==. E.val authId
                E.&&. userAdmin E.^. UserFunctionFunction E.==. E.val SchoolAdmin
                E.&&. course E.^. CourseTerm      E.==. E.val tid
                E.&&. course E.^. CourseSchool    E.==. E.val ssh
                E.&&. course E.^. CourseShorthand E.==. E.val csh
        guardMExceptT isAdmin $ unauthorizedI MsgUnauthorizedSchoolAdmin
        return Authorized
      -- Allocations: access only to school admins
      AllocationR tid ssh ash _ -> $cachedHereBinary (mAuthId, tid, ssh, ash) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isAdmin <- lift . E.selectExists . E.from $ \(allocation `E.InnerJoin` userAdmin) -> do
          E.on $ allocation E.^. AllocationSchool E.==. userAdmin E.^. UserFunctionSchool
          E.where_  $ userAdmin E.^. UserFunctionUser E.==. E.val authId
                E.&&. userAdmin E.^. UserFunctionFunction E.==. E.val SchoolAdmin
                E.&&. allocation E.^. AllocationTerm      E.==. E.val tid
                E.&&. allocation E.^. AllocationSchool    E.==. E.val ssh
                E.&&. allocation E.^. AllocationShorthand E.==. E.val ash
        guardMExceptT isAdmin (unauthorizedI MsgUnauthorizedSchoolAdmin)
        return Authorized
      -- Schools: access only to school admins
      SchoolR ssh _ -> $cachedHereBinary (mAuthId, ssh) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isAdmin <- lift . existsBy $ UniqueUserFunction authId ssh SchoolAdmin
        guardMExceptT isAdmin (unauthorizedI MsgUnauthorizedSchoolAdmin)
        return Authorized
      -- other routes: access to any admin is granted here
      _other -> $cachedHereBinary mAuthId . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        adrights <- lift $ selectFirst [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolAdmin] []
        guardMExceptT (isJust adrights) (unauthorizedI MsgUnauthorizedSiteAdmin)
        return Authorized
tagAccessPredicate AuthSystemExamOffice = cacheAPSystemFunction SystemExamOffice (Just $ Right diffHour) $ \mAuthId' _ _ examOfficeList -> if
  | maybe True (`Set.notMember` examOfficeList) mAuthId' -> Right $ if
      | is _Nothing mAuthId' -> return AuthenticationRequired
      | otherwise -> unauthorizedI MsgUnauthorizedSystemExamOffice
  | otherwise -> Left $ APDB $ \_ _ mAuthId _ _ -> $cachedHereBinary mAuthId . exceptT return return $ do
      authId <- maybeExceptT AuthenticationRequired $ return mAuthId
      isExamOffice <- lift $ exists [UserSystemFunctionUser ==. authId, UserSystemFunctionFunction ==. SystemExamOffice, UserSystemFunctionIsOptOut ==. False]
      guardMExceptT isExamOffice $ unauthorizedI MsgUnauthorizedSystemExamOffice
      return Authorized
tagAccessPredicate AuthStudent = cacheAPSystemFunction SystemStudent (Just $ Right diffHour) $ \mAuthId' _ _ studentList -> if
  | maybe True (`Set.notMember` studentList) mAuthId' -> Right $ if
      | is _Nothing mAuthId' -> return AuthenticationRequired
      | otherwise -> unauthorizedI MsgUnauthorizedStudent
  | otherwise -> Left $ APDB $ \_ _ mAuthId _ _ -> $cachedHereBinary mAuthId . exceptT return return $ do
      authId <- maybeExceptT AuthenticationRequired $ return mAuthId
      isStudent <- lift $ exists [UserSystemFunctionUser ==. authId, UserSystemFunctionFunction ==. SystemStudent, UserSystemFunctionIsOptOut ==. False]
      guardMExceptT isStudent $ unauthorizedI MsgUnauthorizedStudent
      return Authorized
tagAccessPredicate AuthExamOffice = cacheAPSchoolFunction SchoolExamOffice (Just $ Right diffHour) $ \mAuthId' route' _ examOfficeList -> if
  | maybe True (`Set.notMember` examOfficeList) mAuthId' -> Right $ case route' of
      _ | is _Nothing mAuthId' -> return AuthenticationRequired
      CExamR{} -> unauthorizedI MsgUnauthorizedExamExamOffice
      EExamR{} -> unauthorizedI MsgUnauthorizedExternalExamExamOffice
      CourseR{} -> unauthorizedI MsgUnauthorizedExamExamOffice
      SchoolR _ _ -> unauthorizedI MsgUnauthorizedSchoolExamOffice
      _other -> unauthorizedI MsgUnauthorizedExamOffice
  | otherwise -> Left $ APDB $ \_ _ mAuthId route _ -> case route of
      CExamR tid ssh csh examn _ -> $cachedHereBinary (mAuthId, tid, ssh, csh, examn) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        hasUsers <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examResult) -> do
          E.on $ examResult E.^. ExamResultExam E.==. exam E.^. ExamId
          E.on $ exam E.^. ExamCourse E.==. course E.^. CourseId

          E.where_ $ course E.^. CourseTerm E.==. E.val tid
               E.&&. course E.^. CourseSchool E.==. E.val ssh
               E.&&. course E.^. CourseShorthand E.==. E.val csh
               E.&&. exam E.^. ExamName E.==. E.val examn

          E.where_ $ examOfficeExamResultAuth (E.val authId) examResult
        guardMExceptT hasUsers (unauthorizedI MsgUnauthorizedExamExamOffice)
        return Authorized
      EExamR tid ssh coursen examn _ -> $cachedHereBinary (mAuthId, tid, ssh, coursen, examn) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        hasUsers <- lift . E.selectExists . E.from $ \(eexam `E.InnerJoin` eexamResult) -> do
          E.on $ eexam E.^. ExternalExamId E.==. eexamResult E.^. ExternalExamResultExam

          E.where_ $ eexam E.^. ExternalExamTerm E.==. E.val tid
               E.&&. eexam E.^. ExternalExamSchool E.==. E.val ssh
               E.&&. eexam E.^. ExternalExamCourseName E.==. E.val coursen
               E.&&. eexam E.^. ExternalExamExamName E.==. E.val examn

          E.where_ $ examOfficeExternalExamResultAuth (E.val authId) eexamResult
        guardMExceptT hasUsers $ unauthorizedI MsgUnauthorizedExternalExamExamOffice
        return Authorized
      CourseR _ ssh _ _ -> $cachedHereBinary (mAuthId, ssh) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isExamOffice <- lift . existsBy $ UniqueUserFunction authId ssh SchoolExamOffice
        guardMExceptT isExamOffice $ unauthorizedI MsgUnauthorizedExamExamOffice
        return Authorized
      SchoolR ssh _ -> $cachedHereBinary (mAuthId, ssh) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isExamOffice <- lift . existsBy $ UniqueUserFunction authId ssh SchoolExamOffice
        guardMExceptT isExamOffice (unauthorizedI MsgUnauthorizedSchoolExamOffice)
        return Authorized
      _other -> $cachedHereBinary mAuthId . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isExamOffice <- lift $ exists [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolExamOffice]
        guardMExceptT isExamOffice (unauthorizedI MsgUnauthorizedExamOffice)
        return Authorized
tagAccessPredicate AuthEvaluation = cacheAPSchoolFunction SchoolEvaluation (Just $ Right diffHour) $ \mAuthId' _ _ evaluationList -> if
  | maybe True (`Set.notMember` evaluationList) mAuthId' -> Right $ if
      | is _Nothing mAuthId' -> return AuthenticationRequired
      | otherwise -> unauthorizedI MsgUnauthorizedEvaluation
  | otherwise -> Left $ APDB $ \_ _ mAuthId route _ -> case route of
      ParticipantsR _ ssh -> $cachedHereBinary (mAuthId, ssh) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isEvaluation <- lift . existsBy $ UniqueUserFunction authId ssh SchoolEvaluation
        guardMExceptT isEvaluation $ unauthorizedI MsgUnauthorizedEvaluation
        return Authorized
      CourseR _ ssh _ _ -> $cachedHereBinary(mAuthId, ssh) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isEvaluation <- lift . existsBy $ UniqueUserFunction authId ssh SchoolEvaluation
        guardMExceptT isEvaluation $ unauthorizedI MsgUnauthorizedEvaluation
        return Authorized
      _other -> $cachedHereBinary mAuthId . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isEvaluation <- lift $ exists [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolEvaluation]
        guardMExceptT isEvaluation $ unauthorizedI MsgUnauthorizedEvaluation
        return Authorized
tagAccessPredicate AuthAllocationAdmin = cacheAPSchoolFunction SchoolAllocation (Just $ Right diffHour) $ \mAuthId' _ _ allocationList -> if
  | maybe True (`Set.notMember` allocationList) mAuthId' -> Right $ if
      | is _Nothing mAuthId' -> return AuthenticationRequired
      | otherwise -> unauthorizedI MsgUnauthorizedAllocationAdmin
  | otherwise -> Left $ APDB $ \_ _ mAuthId route _ -> case route of
      AllocationR _ ssh _ _ -> $cachedHereBinary (mAuthId, ssh) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isEvaluation <- lift . existsBy $ UniqueUserFunction authId ssh SchoolAllocation
        guardMExceptT isEvaluation $ unauthorizedI MsgUnauthorizedAllocationAdmin
        return Authorized
      CourseR _ ssh _ _ -> $cachedHereBinary (mAuthId, ssh) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isEvaluation <- lift . existsBy $ UniqueUserFunction authId ssh SchoolAllocation
        guardMExceptT isEvaluation $ unauthorizedI MsgUnauthorizedAllocationAdmin
        return Authorized
      _other -> $cachedHereBinary mAuthId . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isEvaluation <- lift $ exists [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolAllocation]
        guardMExceptT isEvaluation $ unauthorizedI MsgUnauthorizedAllocationAdmin
        return Authorized
tagAccessPredicate AuthToken = APDB $ \_ _ mAuthId route isWrite -> exceptT return return $
  lift . validateBearer mAuthId route isWrite =<< askBearerUnsafe
tagAccessPredicate AuthNoEscalation = APDB $ \_ _ mAuthId route _ -> case route of
  AdminHijackUserR cID -> $cachedHereBinary (mAuthId, cID) . exceptT return return $ do
    myUid <- maybeExceptT AuthenticationRequired $ return mAuthId
    uid <- decrypt cID
    otherSchoolsFunctions <- lift . $cachedHereBinary uid $ Set.fromList . map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid] []
    mySchools <- lift . $cachedHereBinary myUid $ Set.fromList . map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. myUid, UserFunctionFunction ==. SchoolAdmin] []
    guardMExceptT (otherSchoolsFunctions `Set.isSubsetOf` mySchools) (unauthorizedI MsgUnauthorizedAdminEscalation)
    return Authorized
  r -> $unsupportedAuthPredicate AuthNoEscalation r
tagAccessPredicate AuthDeprecated = APHandler $ \_ r _ -> do
  $logWarnS "AccessControl" ("deprecated route: " <> tshow r)
  addMessageI Error MsgDeprecatedRoute
  allow <- getsYesod $ view _appAllowDeprecated
  return $ bool (Unauthorized "Deprecated Route") Authorized allow
tagAccessPredicate AuthDevelopment = APHandler $ \_ r _ -> do
  $logWarnS "AccessControl" ("route in development: " <> tshow r)
#ifdef DEVELOPMENT
  return Authorized
#else
  return $ Unauthorized "Route under development"
#endif

tagAccessPredicate AuthLecturer = cacheAPDB' (Just $ Right diffMinute) mkLecturerList $ \mAuthId' route' _ mLecturerList -> if
  | Just lecturerList <- mLecturerList
  , maybe True (`Set.notMember` lecturerList) mAuthId' -> Right $ case route' of
      _ | is _Nothing mAuthId' -> return AuthenticationRequired
      CourseR{} -> unauthorizedI MsgUnauthorizedLecturer
      AllocationR{} -> unauthorizedI MsgUnauthorizedAllocationLecturer
      EExamR{} -> unauthorizedI MsgUnauthorizedExternalExamLecturer
      _other -> unauthorizedI MsgUnauthorizedSchoolLecturer 
  | otherwise -> Left $ APDB $ \_ _ mAuthId route _ -> case route of
      CourseR tid ssh csh _ -> $cachedHereBinary (mAuthId, tid, ssh, csh) . exceptT return return $ do
         authId <- maybeExceptT AuthenticationRequired $ return mAuthId
         isLecturer <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` lecturer) -> do
           E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
           E.where_ $ lecturer E.^. LecturerUser E.==. E.val authId
                E.&&. course E.^. CourseTerm      E.==. E.val tid
                E.&&. course E.^. CourseSchool    E.==. E.val ssh
                E.&&. course E.^. CourseShorthand E.==. E.val csh
         guardMExceptT isLecturer (unauthorizedI MsgUnauthorizedLecturer)
         return Authorized
      AllocationR tid ssh ash _ -> $cachedHereBinary (mAuthId, tid, ssh, ash) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isLecturer <- lift . E.selectExists . E.from $ \(allocation `E.InnerJoin` allocationCourse `E.InnerJoin` course `E.InnerJoin` lecturer) -> do
          E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
          E.on $ course E.^. CourseId E.==. allocationCourse E.^. AllocationCourseCourse
          E.on $ allocation E.^. AllocationId E.==. allocationCourse E.^. AllocationCourseAllocation
          E.where_ $ lecturer E.^. LecturerUser E.==. E.val authId
               E.&&. allocation E.^. AllocationTerm      E.==. E.val tid
               E.&&. allocation E.^. AllocationSchool    E.==. E.val ssh
               E.&&. allocation E.^. AllocationShorthand E.==. E.val ash
        guardMExceptT isLecturer $ unauthorizedI MsgUnauthorizedAllocationLecturer
        return Authorized
      EExamR tid ssh coursen examn _ -> $cachedHereBinary (mAuthId, tid, ssh, coursen, examn) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isLecturer <- lift . E.selectExists . E.from $ \(eexam `E.InnerJoin` staff) -> do
          E.on $ eexam E.^. ExternalExamId E.==. staff E.^. ExternalExamStaffExam
          E.where_ $ staff E.^. ExternalExamStaffUser E.==. E.val authId
               E.&&. eexam E.^. ExternalExamTerm E.==. E.val tid
               E.&&. eexam E.^. ExternalExamSchool E.==. E.val ssh
               E.&&. eexam E.^. ExternalExamCourseName E.==. E.val coursen
               E.&&. eexam E.^. ExternalExamExamName E.==. E.val examn
        guardMExceptT isLecturer $ unauthorizedI MsgUnauthorizedExternalExamLecturer
        return Authorized
      -- lecturer for any school will do
      _ -> $cachedHereBinary mAuthId . exceptT return return $ do
         authId <- maybeExceptT AuthenticationRequired $ return mAuthId
         void . maybeMExceptT (unauthorizedI MsgUnauthorizedSchoolLecturer) $ selectFirst [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolLecturer] []
         return Authorized
  where
    mkLecturerList _ route _ = case route of
      CourseR{} -> cacheLecturerList 
      AllocationR{} -> cacheLecturerList 
      EExamR{} -> Just
        ( AuthCacheExternalExamStaffList
        , fmap (setOf $ folded . _Value) . E.select . E.from $ return . (E.^. ExternalExamStaffUser)
        )
      _other -> Just
        ( AuthCacheSchoolFunctionList SchoolLecturer
        , fmap (setOf $ folded . _Value) . E.select . E.from $ \userFunction -> do
            E.where_ $ userFunction E.^. UserFunctionFunction E.==. E.val SchoolLecturer
            return $ userFunction E.^. UserFunctionUser
        )
      where
        cacheLecturerList = Just
          ( AuthCacheLecturerList
          , fmap (setOf $ folded . _Value) . E.select . E.from $ return . (E.^. LecturerUser)
          )
tagAccessPredicate AuthCorrector = cacheAPDB (Just $ Right diffMinute) AuthCacheCorrectorList mkCorrectorList $ \mAuthId' route' _ correctorList -> if
  | maybe True (`Set.notMember` correctorList) mAuthId' -> Right $ case route' of
      _ | is _Nothing mAuthId' -> return AuthenticationRequired
      CSubmissionR{} -> unauthorizedI MsgUnauthorizedSubmissionCorrector
      CSheetR{} -> unauthorizedI MsgUnauthorizedSheetCorrector
      CourseR{} -> unauthorizedI MsgUnauthorizedCorrector
      _other -> unauthorizedI MsgUnauthorizedCorrectorAny
  | otherwise -> Left $ APDB $ \_ _ mAuthId route _ -> exceptT return return $ do
      authId <- maybeExceptT AuthenticationRequired $ return mAuthId
      case route of
        CSubmissionR _ _ _ _ cID _ -> lift . $cachedHereBinary (authId, cID) . maybeT (unauthorizedI MsgUnauthorizedSubmissionCorrector) $ do
          sid <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
          guardM . lift . E.selectExists . E.from $ \submission ->
            E.where_ $ submission E.^. SubmissionId E.==. E.val sid
                 E.&&. submission E.^. SubmissionRatingBy E.==. E.justVal authId
          return Authorized
        CSheetR tid ssh csh shn _ -> lift . $cachedHereBinary (authId, tid, ssh, csh, shn) . maybeT (unauthorizedI MsgUnauthorizedSheetCorrector) $ do
          guardM . lift . E.selectExists . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` sheetCorrector) -> do
            E.on $ sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId
            E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
            E.where_ $ sheetCorrector E.^. SheetCorrectorUser E.==. E.val authId
            E.where_ $ course E.^. CourseTerm E.==. E.val tid
                 E.&&. course E.^. CourseSchool E.==. E.val ssh
                 E.&&. course E.^. CourseShorthand E.==. E.val csh
                 E.&&. sheet E.^. SheetName E.==. E.val shn
          return Authorized
        CourseR tid ssh csh _ -> lift . $cachedHereBinary (mAuthId, tid, ssh, csh) . maybeT (unauthorizedI MsgUnauthorizedCorrector) $ do
          guardM . lift . E.selectExists . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` sheetCorrector) -> do
            E.on $ sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId
            E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
            E.where_ $ sheetCorrector E.^. SheetCorrectorUser E.==. E.val authId
            E.where_ $ course E.^. CourseTerm E.==. E.val tid
                 E.&&. course E.^. CourseSchool E.==. E.val ssh
                 E.&&. course E.^. CourseShorthand E.==. E.val csh
          return Authorized
        _ -> lift . $cachedHereBinary mAuthId . maybeT (unauthorizedI MsgUnauthorizedCorrectorAny) $ do
          guardM . lift . E.selectExists . E.from $ \sheetCorrector ->
            E.where_ $ sheetCorrector E.^. SheetCorrectorUser E.==. E.val authId
          return Authorized
  where
    mkCorrectorList = do
      submissionCorrectors <- E.select . E.from $ \submission -> E.distinctOnOrderBy [E.asc $ submission E.^. SubmissionRatingBy] $ do
        E.where_ . E.isJust $ submission E.^. SubmissionRatingBy
        return $ submission E.^. SubmissionRatingBy
      let submissionCorrectors' = Set.fromDistinctAscList $ mapMaybe (preview $ _Value . _Just) submissionCorrectors

      sheetCorrectors <- E.select . E.from $ \sheetCorrector -> E.distinctOnOrderBy [E.asc $ sheetCorrector E.^. SheetCorrectorUser] $
        return $ sheetCorrector E.^. SheetCorrectorUser
      let sheetCorrectors' = Set.fromDistinctAscList $ map (^. _Value) sheetCorrectors

      return $ submissionCorrectors' `Set.union` sheetCorrectors'
tagAccessPredicate AuthExamCorrector = cacheAPDB (Just $ Right diffMinute) AuthCacheExamCorrectorList mkExamCorrectorList $ \mAuthId' route' _ examCorrectorList -> if
  | maybe True (`Set.notMember` examCorrectorList) mAuthId' -> Right $ case route' of
      _ | is _Nothing mAuthId' -> return AuthenticationRequired
      CExamR{} -> unauthorizedI MsgUnauthorizedExamCorrector
      CourseR{} -> unauthorizedI MsgUnauthorizedExamCorrector
      r -> $unsupportedAuthPredicate AuthExamCorrector r
  | otherwise -> Left $ APDB $ \_ _ mAuthId route _ -> case route of
      CExamR tid ssh csh examn _ -> $cachedHereBinary (mAuthId, tid, ssh, csh, examn) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isCorrector <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examCorrector) -> do
          E.on $ examCorrector E.^. ExamCorrectorExam E.==. exam E.^. ExamId
          E.on $ exam E.^. ExamCourse E.==. course E.^. CourseId
          E.where_ $ examCorrector E.^. ExamCorrectorUser E.==. E.val authId
            E.&&. course E.^. CourseTerm      E.==. E.val tid
            E.&&. course E.^. CourseSchool    E.==. E.val ssh
            E.&&. course E.^. CourseShorthand E.==. E.val csh
            E.&&. exam   E.^. ExamName        E.==. E.val examn
        guardMExceptT isCorrector $ unauthorizedI MsgUnauthorizedExamCorrector
        return Authorized
      CourseR tid ssh csh _ -> $cachedHereBinary (tid, ssh, csh) . exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isCorrector <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examCorrector) -> do
          E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
          E.on $ exam E.^. ExamId E.==. examCorrector E.^. ExamCorrectorExam
          E.where_ $ examCorrector E.^. ExamCorrectorUser E.==. E.val authId
            E.&&. course E.^. CourseTerm      E.==. E.val tid
            E.&&. course E.^. CourseSchool    E.==. E.val ssh
            E.&&. course E.^. CourseShorthand E.==. E.val csh
        guardMExceptT isCorrector $ unauthorizedI MsgUnauthorizedExamCorrector
        return Authorized
      r -> $unsupportedAuthPredicate AuthExamCorrector r
  where
    mkExamCorrectorList = fmap (setOf $ folded . _Value) . E.select . E.from $ return . (E.^. ExamCorrectorUser)
tagAccessPredicate AuthTutor = cacheAPDB (Just $ Right diffMinute) AuthCacheTutorList mkTutorList $ \mAuthId' route' _ tutorList -> if
  | maybe True (`Set.notMember` tutorList) mAuthId' -> Right $ case route' of
      _ | is _Nothing mAuthId' -> return AuthenticationRequired
      CTutorialR{} -> unauthorizedI MsgUnauthorizedTutorialTutor
      CourseR{} -> unauthorizedI MsgUnauthorizedCourseTutor
      _other -> unauthorizedI MsgUnauthorizedTutor
  | otherwise -> Left $ APDB $ \_ _ mAuthId route _ -> exceptT return return $ do
      authId <- maybeExceptT AuthenticationRequired $ return mAuthId
      resList <- $cachedHereBinary authId . lift . E.select . E.from $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutor) -> do
        E.on $ tutor E.^. TutorTutorial E.==. tutorial E.^. TutorialId
        E.on $ tutorial E.^. TutorialCourse E.==. course E.^. CourseId
        E.where_ $ tutor E.^. TutorUser E.==. E.val authId
        return (course E.^. CourseId, tutorial E.^. TutorialId)
      let
        resMap :: Map CourseId (Set TutorialId)
        resMap = Map.fromListWith Set.union [ (cid, Set.singleton tutid) | (E.Value cid, E.Value tutid) <- resList ]
      case route of
        CTutorialR tid ssh csh tutn _ -> maybeT (unauthorizedI MsgUnauthorizedTutorialTutor) $ do
          Entity cid _ <- $cachedHereBinary (tid, ssh, csh) . MaybeT . lift . getBy $ TermSchoolCourseShort tid ssh csh
          Entity tutid _ <- $cachedHereBinary (cid, tutn) . MaybeT . lift . getBy $ UniqueTutorial cid tutn
          guard $ tutid `Set.member` fromMaybe Set.empty (resMap !? cid)
          return Authorized
        CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgUnauthorizedCourseTutor) $ do
          Entity cid _ <- $cachedHereBinary (tid, ssh, csh) . MaybeT . lift . getBy $ TermSchoolCourseShort tid ssh csh
          guard $ cid `Set.member` Map.keysSet resMap
          return Authorized
        _ -> do
          guardMExceptT (not $ Map.null resMap) (unauthorizedI MsgUnauthorizedTutor)
          return Authorized
  where
    mkTutorList = fmap (setOf $ folded . _Value) . E.select . E.from $ return . (E.^. TutorUser)
tagAccessPredicate AuthTutorControl = APDB $ \_ _ _ route _ -> case route of
  CTutorialR tid ssh csh tutn _ -> maybeT (unauthorizedI MsgUnauthorizedTutorialTutorControl) $ do
    Entity cid _ <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
    Entity _ Tutorial{..} <- $cachedHereBinary (cid, tutn) . MaybeT . getBy $ UniqueTutorial cid tutn
    guard tutorialTutorControlled
    return Authorized
  r -> $unsupportedAuthPredicate AuthTutorControl r
tagAccessPredicate AuthSubmissionGroup = APDB $ \_ _ mAuthId route _ -> case route of
  CSubmissionR tid ssh csh shn cID _ -> maybeT (unauthorizedI MsgUnauthorizedSubmissionSubmissionGroup) $ do
    course <- MaybeT . $cachedHereBinary (tid, ssh, csh) . getKeyBy $ TermSchoolCourseShort tid ssh csh
    Entity _ Sheet{..} <- MaybeT . $cachedHereBinary (course, shn) . getBy $ CourseSheet course shn
    when (is _RegisteredGroups sheetGrouping) $ do
      smId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
      groups <- $cachedHereBinary cID . lift . fmap (Set.fromList . fmap E.unValue) . E.select . E.from $ \(submissionGroupUser `E.InnerJoin` submissionUser) -> do
        E.on $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. submissionUser E.^. SubmissionUserUser
        E.where_ $ submissionUser E.^. SubmissionUserSubmission E.==. E.val smId
        return $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup
      unless (Set.null groups) $ do
        uid <- hoistMaybe mAuthId
        guardM . lift $ exists [SubmissionGroupUserUser ==. uid, SubmissionGroupUserSubmissionGroup <-. Set.toList groups]
    return Authorized
  CSheetR tid ssh csh sheetn _ -> maybeT (unauthorizedI MsgUnauthorizedSheetSubmissionGroup) $ do
    course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
    Entity _ Sheet{..} <- $cachedHereBinary (course, sheetn) . MaybeT . getBy $ CourseSheet course sheetn
    when (is _RegisteredGroups sheetGrouping) $ do
      uid <- hoistMaybe mAuthId
      guardM . lift . E.selectExists . E.from $ \(submissionGroup `E.InnerJoin` submissionGroupUser) -> do
        E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
        E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val course
             E.&&. submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val uid

    return Authorized
  r -> $unsupportedAuthPredicate AuthSubmissionGroup r
tagAccessPredicate AuthTime = APDB $ \_ (runTACont -> cont) mAuthId route isWrite -> case route of
  CExamR tid ssh csh examn subRoute -> maybeT (unauthorizedI MsgUnauthorizedExamTime) $ do
    course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
    Entity eId Exam{..} <- $cachedHereBinary (course, examn) . MaybeT . getBy $ UniqueExam course examn
    cTime <- liftIO getCurrentTime
    registration <- case mAuthId of
      Just uid -> $cachedHereBinary (eId, uid) . lift . getBy $ UniqueExamRegistration eId uid
      Nothing  -> return Nothing

    let visible = NTop examVisibleFrom <= NTop (Just cTime)

    case subRoute of
      EShowR     -> guard visible
      EUsersR    -> guard $ NTop examStart <= NTop (Just cTime)
                         && NTop (Just cTime) <= NTop examFinished
      ERegisterR
        | is _Nothing registration
          -> guard $ visible
                  && NTop examRegisterFrom <= NTop (Just cTime)
                  && NTop (Just cTime) <= NTop examRegisterTo
        | otherwise
          -> guard $ visible
                  && NTop (Just cTime) <= NTop examDeregisterUntil
      ERegisterOccR occn -> do
        occId <- hoistMaybe <=< $cachedHereBinary (eId, occn) . lift . getKeyBy $ UniqueExamOccurrence eId occn
        if
          | (registration >>= examRegistrationOccurrence . entityVal) == Just occId
            -> guard $ visible
                    && NTop (Just cTime) <= NTop examDeregisterUntil
          | otherwise
            -> guard $ visible
                    && NTop examRegisterFrom <= NTop (Just cTime)
                    && NTop (Just cTime) <= NTop examRegisterTo
      ECorrectR -> guard $ NTop (Just cTime) >= NTop examStart
                        && NTop (Just cTime) <= NTop examFinished
      _          -> return ()

    return Authorized

  CTutorialR tid ssh csh tutn TRegisterR -> maybeT (unauthorizedI MsgUnauthorizedTutorialTime) $ do
    now <- liftIO getCurrentTime
    course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
    Entity tutId Tutorial{..} <- $cachedHereBinary (course, tutn) . MaybeT . getBy $ UniqueTutorial course tutn
    registered <- case mAuthId of
      Just uid -> $cachedHereBinary (tutId, uid) . lift . existsBy $ UniqueTutorialParticipant tutId uid
      Nothing  -> return False

    if
      | not registered
      , maybe False (now >=) tutorialRegisterFrom
      , maybe True  (now <=) tutorialRegisterTo
        -> return Authorized
      | registered
      , maybe True  (now <=) tutorialDeregisterUntil
        -> return Authorized
      | otherwise
        -> mzero

  CSheetR tid ssh csh shn subRoute -> maybeT (unauthorizedI MsgUnauthorizedSheetTime) $ do
    cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
    Entity _sid Sheet{..} <- $cachedHereBinary (cid, shn) . MaybeT . getBy $ CourseSheet cid shn
    cTime <- liftIO getCurrentTime
    let
      visible = NTop sheetVisibleFrom <= NTop (Just cTime)
      active  = NTop sheetActiveFrom <= NTop (Just cTime) && NTop (Just cTime) <= NTop sheetActiveTo
      marking = NTop (Just cTime) > NTop sheetActiveTo

    guard visible

    case subRoute of
      -- Single Files
      SFileR                SheetExercise _ -> guard $ NTop sheetActiveFrom <= NTop (Just cTime)
      SFileR                SheetHint     _ -> guard $ maybe False (<= cTime) sheetHintFrom
      SFileR                SheetSolution _ -> guard $ maybe False (<= cTime) sheetSolutionFrom
      SFileR                _             _ -> mzero
      -- Archives of SheetFileType
      SZipR                 SheetExercise   -> guard $ NTop sheetActiveFrom <= NTop (Just cTime)
      SZipR                 SheetHint       -> guard $ maybe False (<= cTime) sheetHintFrom
      SZipR                 SheetSolution   -> guard $ maybe False (<= cTime) sheetSolutionFrom
      SZipR                 _               -> mzero
      -- Submissions
      SubmissionNewR           -> guard   active
      SAssignR                 -> guard   marking -- Correctors can only be assigned when the Sheet is inactive, since submissions are subject to change; this is assumed in Corrections.assignHandler
      SubmissionR _ SubAssignR -> guard   marking -- Correctors can only be assigned when the Sheet is inactive, since submissions are subject to change
      SubmissionR _ _          -> guard   active
      _                        -> return ()

    return Authorized

  CourseR tid ssh csh (MaterialR mnm _) -> maybeT (unauthorizedI MsgUnauthorizedMaterialTime) $ do
    cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
    Entity _mid Material{materialVisibleFrom} <- $cachedHereBinary (cid, mnm) . MaybeT . getBy $ UniqueMaterial cid mnm
    cTime <- liftIO getCurrentTime
    let visible = NTop materialVisibleFrom <= NTop (Just cTime)
    guard visible
    return Authorized

  CourseR tid ssh csh CRegisterR -> do
    now <- liftIO getCurrentTime
    mbc <- $cachedHereBinary (tid, ssh, csh) . getBy $ TermSchoolCourseShort tid ssh csh
    registered <- cont (predDNFSingleton $ PLVariable AuthCourseRegistered) mAuthId route isWrite
    case mbc of
      (Just (Entity _ Course{courseRegisterFrom, courseRegisterTo}))
        | not registered
        , maybe False (now >=) courseRegisterFrom -- Nothing => no registration allowed
        , maybe True  (now <=) courseRegisterTo      -> return Authorized
      (Just (Entity cid Course{courseDeregisterUntil}))
        | registered
          -> maybeT (unauthorizedI MsgUnauthorizedCourseRegistrationTime) $ do
            guard $ maybe True (now <=) courseDeregisterUntil
            forM_ mAuthId $ \uid -> do
              exams <- lift . E.select . E.from $ \exam -> do
                E.where_ . E.exists . E.from $ \examRegistration ->
                  E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. exam E.^. ExamId
                       E.&&. examRegistration E.^. ExamRegistrationUser E.==. E.val uid
                E.where_ $ exam E.^. ExamCourse E.==. E.val cid
                return $ exam E.^. ExamDeregisterUntil
              forM_ exams $ \(E.Value deregUntil) ->
                guard $ NTop (Just now) <= NTop deregUntil

              tutorials <- lift . E.select . E.from $ \tutorial -> do
                E.where_ . E.exists . E.from $ \tutorialParticipant ->
                  E.where_ $ tutorialParticipant E.^. TutorialParticipantTutorial E.==. tutorial E.^. TutorialId
                       E.&&. tutorialParticipant E.^. TutorialParticipantUser E.==. E.val uid
                E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid
                return $ tutorial E.^. TutorialDeregisterUntil
              forM_ tutorials $ \(E.Value deregUntil) ->
                guard $ NTop (Just now) <= NTop deregUntil
            return Authorized
      _other -> unauthorizedI MsgUnauthorizedCourseRegistrationTime
  
  CApplicationR tid ssh csh _ _ -> maybeT (unauthorizedI MsgUnauthorizedApplicationTime) $ do
    Entity course Course{..} <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
    allocationCourse <- $cachedHereBinary course . lift . getBy $ UniqueAllocationCourse course
    allocation <- for allocationCourse $ \(Entity _ AllocationCourse{..}) -> $cachedHereBinary allocationCourseAllocation . MaybeT $ get allocationCourseAllocation

    case allocation of
      Nothing -> do
        cTime <- liftIO getCurrentTime
        guard $ maybe False (cTime >=) courseRegisterFrom
        guard $ maybe True  (cTime <=) courseRegisterTo
      Just Allocation{..} -> do
        cTime <- liftIO getCurrentTime
        guard $ NTop allocationRegisterFrom <= NTop (Just cTime)
        guard $ NTop (Just cTime) <= NTop allocationRegisterTo

    return Authorized

  AllocationR tid ssh ash _ -> maybeT (unauthorizedI MsgUnauthorizedAllocationRegisterTime) $ do
    -- Checks `registerFrom` and `registerTo`, override as further routes become available
    now <- liftIO getCurrentTime
    Entity _ Allocation{..} <- MaybeT . $cachedHereBinary (tid, ssh, ash) . getBy $ TermSchoolAllocationShort tid ssh ash
    guard $ NTop allocationRegisterFrom <= NTop (Just now)
    guard $ NTop (Just now) <= NTop allocationRegisterTo
    return Authorized

  MessageR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageTime) $ do
    smId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
    cTime <- liftIO getCurrentTime
    let cacheTime = diffDay
        massageVisible = Map.fromList . map (over _1 E.unValue . over (_2 . _1) E.unValue . over (_2 . _2) E.unValue)
    visibleSystemMessages <- lift . memcacheAuth' @(Map SystemMessageId (Maybe UTCTime, Maybe UTCTime)) (Right cacheTime) AuthCacheVisibleSystemMessages . fmap massageVisible . E.select . E.from $ \systemMessage -> do
      E.where_ $ E.maybe E.true (E.>=. E.val cTime) (systemMessage E.^. SystemMessageTo)
           E.&&. E.maybe E.false (E.<=. E.val (realToFrac diffDay `addUTCTime` cTime)) (systemMessage E.^. SystemMessageFrom) -- good enough.
      return
        ( systemMessage E.^. SystemMessageId
        , ( systemMessage E.^. SystemMessageFrom
          , systemMessage E.^. SystemMessageTo
          )
        )
    (msgFrom, msgTo) <- hoistMaybe $ Map.lookup smId visibleSystemMessages
    let cTime' = NTop $ Just cTime
    guard $ NTop msgFrom <= cTime'
         && NTop msgTo   >= cTime'
    return Authorized

  MessageHideR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageTime) $ do
    smId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
    SystemMessage{systemMessageFrom, systemMessageTo} <- $cachedHereBinary smId . MaybeT $ get smId
    cTime <- NTop . Just <$> liftIO getCurrentTime
    guard $ NTop systemMessageFrom <= cTime
         && NTop systemMessageTo   >= cTime
    return Authorized

  CNewsR _ _ _ cID _ -> maybeT (unauthorizedI MsgUnauthorizedCourseNewsTime) $ do
    nId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
    CourseNews{courseNewsVisibleFrom} <- $cachedHereBinary nId . MaybeT $ get nId
    cTime <- NTop . Just <$> liftIO getCurrentTime
    guard $ NTop courseNewsVisibleFrom <= cTime
    return Authorized

  r -> $unsupportedAuthPredicate AuthTime r
tagAccessPredicate AuthStaffTime = APDB $ \_ _ _ route isWrite -> case route of
  CApplicationR tid ssh csh _ _ -> maybeT (unauthorizedI MsgUnauthorizedApplicationTime) $ do
    course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
    allocationCourse <- $cachedHereBinary course . lift . getBy $ UniqueAllocationCourse course
    allocation <- for allocationCourse $ \(Entity _ AllocationCourse{..}) -> $cachedHereBinary allocationCourseAllocation . MaybeT $ get allocationCourseAllocation

    case allocation of
      Nothing -> return ()
      Just Allocation{..} -> do
        cTime <- liftIO getCurrentTime
        guard $ NTop allocationStaffAllocationFrom <= NTop (Just cTime)
        when isWrite $
          guard $ NTop (Just cTime) <= NTop allocationStaffAllocationTo

    return Authorized

  AllocationR tid ssh ash _ -> maybeT (unauthorizedI MsgUnauthorizedAllocationRegisterTime) $ do
    -- Checks `registerFrom` and `registerTo`, override as further routes become available
    now <- liftIO getCurrentTime
    Entity _ Allocation{..} <- MaybeT . $cachedHereBinary (tid, ssh, ash) . getBy $ TermSchoolAllocationShort tid ssh ash
    guard $ NTop allocationStaffAllocationFrom <= NTop (Just now)
    guard $ NTop (Just now) <= NTop allocationStaffAllocationTo
    return Authorized

  r -> $unsupportedAuthPredicate AuthStaffTime r
tagAccessPredicate AuthAllocationTime = APDB $ \_ (runTACont -> cont) mAuthId route isWrite -> case route of
  CourseR tid ssh csh CRegisterR -> do
    now <- liftIO getCurrentTime
    mba <- mbAllocation tid ssh csh
    case mba of
      Nothing -> return Authorized
      Just (_, Allocation{..}) -> do
        registered <- cont (predDNFSingleton $ PLVariable AuthCourseRegistered) mAuthId route isWrite
        if
          | not registered
          , NTop allocationRegisterByCourse >= NTop (Just now)
            -> unauthorizedI MsgUnauthorizedAllocatedCourseRegister
          | registered
          , NTop (Just now) >= NTop allocationOverrideDeregister
            -> unauthorizedI MsgUnauthorizedAllocatedCourseDeregister
          | otherwise
            -> return Authorized

  CourseR tid ssh csh CAddUserR -> do
    now <- liftIO getCurrentTime
    mba <- mbAllocation tid ssh csh
    case mba of
      Just (_, Allocation{..})
        |    NTop allocationRegisterByStaffTo <= NTop (Just now)
          || NTop allocationRegisterByStaffFrom >= NTop (Just now)
          -> unauthorizedI MsgUnauthorizedAllocatedCourseRegister
      _other -> return Authorized

  CourseR tid ssh csh CDeleteR -> do
    now <- liftIO getCurrentTime
    mba <- mbAllocation tid ssh csh
    case mba of
      Just (_, Allocation{..})
        |    NTop allocationRegisterByStaffTo <= NTop (Just now)
          || NTop allocationRegisterByStaffFrom >= NTop (Just now)
          -> unauthorizedI MsgUnauthorizedAllocatedCourseDelete
      _other -> return Authorized

  r -> $unsupportedAuthPredicate AuthAllocationTime r
  where
    mbAllocation tid ssh csh = $cachedHereBinary (tid, ssh, csh) . runMaybeT $ do
      cid <- MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
      Entity _ AllocationCourse{..} <- MaybeT . getBy $ UniqueAllocationCourse cid
      (cid,) <$> MaybeT (get allocationCourseAllocation)
tagAccessPredicate AuthCourseTime = APDB $ \_ _ _mAuthId route _ -> case route of
  CourseR tid ssh csh _ -> exceptT return return $ do
    now <- liftIO getCurrentTime
    courseVisible <- $cachedHereBinary (tid, ssh, csh) . lift . E.selectExists . E.from $ \course -> do
      E.where_ $ course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
           E.&&. courseIsVisible now course E.nothing
    guardMExceptT courseVisible (unauthorizedI MsgUnauthorizedCourseTime)
    return Authorized
  r -> $unsupportedAuthPredicate AuthCourseTime r
tagAccessPredicate AuthExamTime = APDB $ \_ _ _ route _ -> case route of
  CSubmissionR tid ssh csh shn _cID CorrectionR -> maybeT (unauthorizedI MsgUnauthorizedCorrectionExamTime) $ do
    cid <- MaybeT . $cachedHereBinary (tid, ssh, csh) . getKeyBy $ TermSchoolCourseShort tid ssh csh
    Entity _sid Sheet{..} <- MaybeT . $cachedHereBinary (cid, shn) . getBy $ CourseSheet cid shn
    whenIsJust (sheetType ^? _examPart . from _SqlKey) $ \epId -> do
      ExamPart{examPartExam} <- MaybeT . $cachedHereBinary epId $ get epId
      Exam{..} <- MaybeT . $cachedHereBinary examPartExam $ get examPartExam
      now <- liftIO getCurrentTime
      guard $ NTop (Just now) >= NTop examFinished
    return Authorized
  r -> $unsupportedAuthPredicate AuthExamTime r
tagAccessPredicate AuthCourseRegistered = cacheAPDB' (Just $ Right diffMinute) mkAuthCacheCourseRegisteredList $ \mAuthId' route' _ mCourseRegisteredList -> if
  | Just courseRegisteredList <- mCourseRegisteredList
  , maybe True (`Set.notMember` courseRegisteredList) mAuthId' -> Right $ case route' of
      _ | is _Nothing mAuthId' -> return AuthenticationRequired
      CourseR{} -> unauthorizedI MsgUnauthorizedRegistered
      r -> $unsupportedAuthPredicate AuthCourseRegistered r
  | otherwise -> Left $ APDB $ \_ _ mAuthId route _ -> case route of
      CourseR tid ssh csh _ -> exceptT return return $ do
        authId <- maybeExceptT AuthenticationRequired $ return mAuthId
        isRegistered <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` courseParticipant) -> do
          E.on $ course E.^. CourseId E.==. courseParticipant E.^. CourseParticipantCourse
          E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. E.val authId
               E.&&. courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
               E.&&. course E.^. CourseTerm      E.==. E.val tid
               E.&&. course E.^. CourseSchool    E.==. E.val ssh
               E.&&. course E.^. CourseShorthand E.==. E.val csh
        guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered)
        return Authorized
      r -> $unsupportedAuthPredicate AuthCourseRegistered r
  where
    mkAuthCacheCourseRegisteredList _ route _ = case route of
      CourseR tid ssh csh _ -> Just
        ( AuthCacheCourseRegisteredList tid ssh csh
        , fmap (setOf $ folded . _Value) . E.select . E.from $ \(course `E.InnerJoin` courseParticipant) -> do
            E.on $ course E.^. CourseId E.==. courseParticipant E.^. CourseParticipantCourse
            E.where_ $ courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
                 E.&&. course E.^. CourseTerm      E.==. E.val tid
                 E.&&. course E.^. CourseSchool    E.==. E.val ssh
                 E.&&. course E.^. CourseShorthand E.==. E.val csh
            return $ courseParticipant E.^. CourseParticipantUser
        )
      _other -> Nothing
tagAccessPredicate AuthTutorialRegistered = APDB $ \_ _ mAuthId route _ -> case route of
  CTutorialR tid ssh csh tutn _ -> exceptT return return $ do
    authId <- maybeExceptT AuthenticationRequired $ return mAuthId
    isRegistered <- $cachedHereBinary (authId, tid, ssh, csh, tutn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutorialParticipant) -> do
      E.on $ tutorial E.^. TutorialId E.==. tutorialParticipant E.^. TutorialParticipantTutorial
      E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
      E.where_ $ tutorialParticipant E.^. TutorialParticipantUser E.==. E.val authId
           E.&&. course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
           E.&&. tutorial E.^. TutorialName  E.==. E.val tutn
    guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered)
    return Authorized
  CourseR tid ssh csh _ -> exceptT return return $ do
    authId <- maybeExceptT AuthenticationRequired $ return mAuthId
    isRegistered <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutorialParticipant) -> do
      E.on $ tutorial E.^. TutorialId E.==. tutorialParticipant E.^. TutorialParticipantTutorial
      E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
      E.where_ $ tutorialParticipant E.^. TutorialParticipantUser E.==. E.val authId
           E.&&. course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
    guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered)
    return Authorized
  r -> $unsupportedAuthPredicate AuthTutorialRegistered r
tagAccessPredicate AuthExamOccurrenceRegistration = APDB $ \_ _ _ route _ -> case route of
  CExamR tid ssh csh examn _ -> exceptT return return $ do
    isOccurrenceRegistration <- $cachedHereBinary (tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam) -> do
      E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
      E.where_ $ course E.^. CourseTerm       E.==. E.val tid
           E.&&. course E.^. CourseSchool     E.==. E.val ssh
           E.&&. course E.^. CourseShorthand  E.==. E.val csh
           E.&&. exam E.^. ExamName           E.==. E.val examn
           E.&&. exam E.^. ExamOccurrenceRule E.==. E.val ExamRoomFifo
    guardMExceptT isOccurrenceRegistration (unauthorizedI MsgUnauthorizedExamOccurrenceRegistration)
    return Authorized
  r -> $unsupportedAuthPredicate AuthExamOccurrenceRegistration r
tagAccessPredicate AuthExamOccurrenceRegistered = APDB $ \_ _ mAuthId route _ -> case route of
  CExamR tid ssh csh examn (ERegisterOccR occn) -> exceptT return return $ do
    authId <- maybeExceptT AuthenticationRequired $ return mAuthId
    hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh, examn, occn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration `E.InnerJoin` examOccurrence) -> do
      E.on $ E.just (examOccurrence E.^. ExamOccurrenceId) E.==. examRegistration E.^. ExamRegistrationOccurrence
      E.on $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
      E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
      E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId
           E.&&. examOccurrence E.^. ExamOccurrenceName E.==. E.val occn
           E.&&. course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
           E.&&. exam E.^. ExamName          E.==. E.val examn
    guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered)
    return Authorized
  CExamR tid ssh csh examn _ -> exceptT return return $ do
    authId <- maybeExceptT AuthenticationRequired $ return mAuthId
    hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
      E.on $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
      E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
      E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId
           E.&&. course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
           E.&&. exam E.^. ExamName          E.==. E.val examn
           E.&&. E.not_ (E.isNothing $ examRegistration E.^. ExamRegistrationOccurrence)
    guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered)
    return Authorized
  CourseR tid ssh csh _ -> exceptT return return $ do
    authId <- maybeExceptT AuthenticationRequired $ return mAuthId
    hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
      E.on $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
      E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
      E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId
           E.&&. course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
           E.&&. E.not_ (E.isNothing $ examRegistration E.^. ExamRegistrationOccurrence)
    guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered)
    return Authorized
  r -> $unsupportedAuthPredicate AuthExamOccurrenceRegistered r
tagAccessPredicate AuthExamRegistered = APDB $ \_ _ mAuthId route _ -> case route of
  CExamR tid ssh csh examn _ -> exceptT return return $ do
    authId <- maybeExceptT AuthenticationRequired $ return mAuthId
    hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
      E.on $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
      E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
      E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId
           E.&&. course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
           E.&&. exam E.^. ExamName          E.==. E.val examn
    guardMExceptT hasRegistration $ unauthorizedI MsgUnauthorizedRegisteredExam
    return Authorized
  CSheetR tid ssh csh shn _ -> exceptT return return $ do
    requiredExam' <- $cachedHereBinary (tid, ssh, csh, shn) . lift . E.selectMaybe . E.from $ \(course `E.InnerJoin` sheet) -> do
      E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
      E.where_ $ course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
           E.&&. sheet  E.^. SheetName       E.==. E.val shn
      return $ sheet E.^. SheetRequireExamRegistration
    requiredExam <- maybeMExceptT (unauthorizedI MsgUnauthorizedRegisteredExam) . return $ E.unValue <$> requiredExam'
    whenIsJust requiredExam $ \eId -> do
      authId <- maybeExceptT AuthenticationRequired $ return mAuthId
      isRegistered <- $cachedHereBinary (authId, eId) . lift . E.selectExists . E.from $ \examRegistration ->
        E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eId
             E.&&. examRegistration E.^. ExamRegistrationUser E.==. E.val authId
      guardMExceptT isRegistered $ unauthorizedI MsgUnauthorizedRegisteredExam
    return Authorized
  CourseR tid ssh csh _ -> exceptT return return $ do
    authId <- maybeExceptT AuthenticationRequired $ return mAuthId
    hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
      E.on $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
      E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
      E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId
           E.&&. course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
    guardMExceptT hasRegistration $ unauthorizedI MsgUnauthorizedRegisteredAnyExam
    return Authorized
  r -> $unsupportedAuthPredicate AuthExamRegistered r
tagAccessPredicate AuthExamResult = APDB $ \_ _ mAuthId route _ -> case route of
  CExamR tid ssh csh examn _ -> exceptT return return $ do
    authId <- maybeExceptT AuthenticationRequired $ return mAuthId
    hasResult <- $cachedHereBinary (authId, tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examResult) -> do
      E.on $ exam E.^. ExamId E.==. examResult E.^. ExamResultExam
      E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
      E.where_ $ examResult E.^. ExamResultUser E.==. E.val authId
           E.&&. course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
           E.&&. exam E.^. ExamName  E.==. E.val examn
    hasPartResult <- $cachedHereBinary (authId, tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examPart `E.InnerJoin` examPartResult) -> do
      E.on $ examPartResult E.^. ExamPartResultExamPart E.==. examPart E.^. ExamPartId
      E.on $ exam E.^. ExamId E.==. examPart E.^. ExamPartExam
      E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
      E.where_ $ examPartResult E.^. ExamPartResultUser E.==. E.val authId
           E.&&. course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
           E.&&. exam E.^. ExamName  E.==. E.val examn
    guardMExceptT (hasResult || hasPartResult) (unauthorizedI MsgUnauthorizedExamResult)
    return Authorized
  EExamR tid ssh coursen examn _ -> $cachedHereBinary (mAuthId, tid, ssh, coursen, examn) . exceptT return return $ do
    authId <- maybeExceptT AuthenticationRequired $ return mAuthId
    hasResult <- $cachedHereBinary (authId, tid, ssh, coursen, examn) . lift . E.selectExists . E.from $ \(eexam `E.InnerJoin` eexamResult) -> do
      E.on $ eexam E.^. ExternalExamId E.==. eexamResult E.^. ExternalExamResultExam
      E.where_ $ eexamResult E.^. ExternalExamResultUser E.==. E.val authId
           E.&&. eexam E.^. ExternalExamTerm E.==. E.val tid
           E.&&. eexam E.^. ExternalExamSchool E.==. E.val ssh
           E.&&. eexam E.^. ExternalExamCourseName E.==. E.val coursen
           E.&&. eexam E.^. ExternalExamExamName E.==. E.val examn
    guardMExceptT hasResult $ unauthorizedI MsgUnauthorizedExternalExamResult
    return Authorized
  CourseR tid ssh csh _ -> exceptT return return $ do
    authId <- maybeExceptT AuthenticationRequired $ return mAuthId
    hasResult <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examResult) -> do
      E.on $ exam E.^. ExamId E.==. examResult E.^. ExamResultExam
      E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
      E.where_ $ examResult E.^. ExamResultUser E.==. E.val authId
           E.&&. course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
    hasPartResult <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examPart `E.InnerJoin` examPartResult) -> do
      E.on $ examPartResult E.^. ExamPartResultExamPart E.==. examPart E.^. ExamPartId
      E.on $ exam E.^. ExamId E.==. examPart E.^. ExamPartExam
      E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
      E.where_ $ examPartResult E.^. ExamPartResultUser E.==. E.val authId
           E.&&. course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
    guardMExceptT (hasResult || hasPartResult) (unauthorizedI MsgUnauthorizedExamResult)
    return Authorized
  r -> $unsupportedAuthPredicate AuthExamRegistered r
tagAccessPredicate AuthAllocationRegistered = APDB $ \_ _ mAuthId route _ -> case route of
  AllocationR tid ssh ash _ -> maybeT (unauthorizedI MsgUnauthorizedAllocationRegistered) $ do
    uid <- hoistMaybe mAuthId
    aId <- MaybeT . $cachedHereBinary (tid, ssh, ash) . getKeyBy $ TermSchoolAllocationShort tid ssh ash
    void . MaybeT . $cachedHereBinary (uid, aId) . getKeyBy $ UniqueAllocationUser aId uid
    return Authorized
  r -> $unsupportedAuthPredicate AuthAllocationRegistered r
tagAccessPredicate AuthParticipant = APDB $ \_ _ mAuthId route _ -> case route of
  CNewsR tid ssh csh cID _ -> maybeT (unauthorizedI MsgUnauthorizedParticipantSelf) $ do
    nId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
    CourseNews{courseNewsParticipantsOnly} <- $cachedHereBinary nId . MaybeT $ get nId
    if | courseNewsParticipantsOnly -> do
           uid <- hoistMaybe mAuthId
           exceptT return (const mzero) . hoist lift $ isCourseParticipant tid ssh csh uid True
       | otherwise
           -> return Authorized

  CourseR tid ssh csh (CUserR cID) -> exceptT return return $ do
    participant <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedParticipant) (const True :: CryptoIDError -> Bool) $ decrypt cID
    isCourseParticipant tid ssh csh participant False
    unauthorizedI MsgUnauthorizedParticipant

  r -> $unsupportedAuthPredicate AuthParticipant r

  where
    isCourseParticipant tid ssh csh participant onlyActive = do
      let
        authorizedIfExists :: E.From a => (a -> E.SqlQuery b) -> ExceptT AuthResult (ReaderT SqlReadBackend (HandlerFor UniWorX)) ()
        authorizedIfExists = flip whenExceptT Authorized <=< lift . E.selectExists . E.from
      -- participant is currently registered
      mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` courseParticipant) -> do
        E.on $ course E.^. CourseId E.==. courseParticipant E.^. CourseParticipantCourse
        E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. E.val participant
             E.&&. course E.^. CourseTerm      E.==. E.val tid
             E.&&. course E.^. CourseSchool    E.==. E.val ssh
             E.&&. course E.^. CourseShorthand E.==. E.val csh
        when onlyActive $
          E.where_ $ courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
      -- participant has at least one submission
      unless onlyActive $
        mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` sheet `E.InnerJoin` submission `E.InnerJoin` submissionUser) -> do
          E.on $ submission E.^. SubmissionId E.==. submissionUser E.^. SubmissionUserSubmission
          E.on $ sheet  E.^. SheetId   E.==. submission E.^. SubmissionSheet
          E.on $ course E.^. CourseId  E.==. sheet E.^. SheetCourse
          E.where_ $ submissionUser E.^. SubmissionUserUser E.==. E.val participant
               E.&&. course E.^. CourseTerm      E.==. E.val tid
               E.&&. course E.^. CourseSchool    E.==. E.val ssh
               E.&&. course E.^. CourseShorthand E.==. E.val csh
      -- participant is member of a submissionGroup
      unless onlyActive $
        mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` submissionGroup `E.InnerJoin` submissionGroupUser) -> do
          E.on $ submissionGroup E.^. SubmissionGroupId  E.==. submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup
          E.on $ course E.^. CourseId  E.==. submissionGroup E.^. SubmissionGroupCourse
          E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val participant
               E.&&. course E.^. CourseTerm      E.==. E.val tid
               E.&&. course E.^. CourseSchool    E.==. E.val ssh
               E.&&. course E.^. CourseShorthand E.==. E.val csh
      -- participant is a sheet corrector
      mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` sheet `E.InnerJoin` sheetCorrector) -> do
        E.on $ sheet  E.^. SheetId   E.==. sheetCorrector E.^. SheetCorrectorSheet
        E.on $ course E.^. CourseId  E.==. sheet E.^. SheetCourse
        E.where_ $ sheetCorrector E.^. SheetCorrectorUser E.==. E.val participant
             E.&&. course E.^. CourseTerm      E.==. E.val tid
             E.&&. course E.^. CourseSchool    E.==. E.val ssh
             E.&&. course E.^. CourseShorthand E.==. E.val csh
      -- participant is a tutorial user
      unless onlyActive $
        mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutorialUser) -> do
          E.on $ tutorial  E.^. TutorialId   E.==. tutorialUser E.^. TutorialParticipantTutorial
          E.on $ course E.^. CourseId  E.==. tutorial E.^. TutorialCourse
          E.where_ $ tutorialUser E.^. TutorialParticipantUser E.==. E.val participant
               E.&&. course E.^. CourseTerm      E.==. E.val tid
               E.&&. course E.^. CourseSchool    E.==. E.val ssh
               E.&&. course E.^. CourseShorthand E.==. E.val csh
      -- participant is tutor for this course
      mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutor) -> do
        E.on $ tutorial  E.^. TutorialId   E.==. tutor E.^. TutorTutorial
        E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
        E.where_ $ tutor E.^. TutorUser E.==. E.val participant
             E.&&. course E.^. CourseTerm      E.==. E.val tid
             E.&&. course E.^. CourseSchool    E.==. E.val ssh
             E.&&. course E.^. CourseShorthand E.==. E.val csh
      -- participant is exam corrector for this course
      mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` exam `E.InnerJoin` examCorrector) -> do
        E.on $ exam E.^. ExamId E.==. examCorrector E.^. ExamCorrectorExam
        E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
        E.where_ $ examCorrector E.^. ExamCorrectorUser E.==. E.val participant
             E.&&. course E.^. CourseTerm      E.==. E.val tid
             E.&&. course E.^. CourseSchool    E.==. E.val ssh
             E.&&. course E.^. CourseShorthand E.==. E.val csh
      -- participant is lecturer for this course
      mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` lecturer) -> do
        E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
        E.where_ $ lecturer E.^. LecturerUser E.==. E.val participant
             E.&&. course E.^. CourseTerm      E.==. E.val tid
             E.&&. course E.^. CourseSchool    E.==. E.val ssh
             E.&&. course E.^. CourseShorthand E.==. E.val csh
      -- participant has an exam result for this course
      unless onlyActive $
        mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` exam `E.InnerJoin` examResult) -> do
          E.on $ examResult E.^. ExamResultExam E.==. exam E.^. ExamId
          E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
          E.where_ $ examResult E.^. ExamResultUser E.==. E.val participant
               E.&&. course E.^. CourseTerm      E.==. E.val tid
               E.&&. course E.^. CourseSchool    E.==. E.val ssh
               E.&&. course E.^. CourseShorthand E.==. E.val csh
      -- participant is registered for an exam for this course
      unless onlyActive $
        mapExceptT ($cachedHereBinary (participant, tid, ssh, csh)) . authorizedIfExists $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
          E.on $ examRegistration E.^. ExamRegistrationExam E.==. exam E.^. ExamId
          E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
          E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val participant
               E.&&. course E.^. CourseTerm      E.==. E.val tid
               E.&&. course E.^. CourseSchool    E.==. E.val ssh
               E.&&. course E.^. CourseShorthand E.==. E.val csh
tagAccessPredicate AuthApplicant = APDB $ \_ _ mAuthId route _ -> case route of
  CourseR tid ssh csh (CUserR cID) -> maybeT (unauthorizedI MsgUnauthorizedApplicant) $ do
    uid <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
    isApplicant <- isCourseApplicant tid ssh csh uid
    guard isApplicant
    return Authorized

  CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgUnauthorizedApplicantSelf) $ do
    uid <- hoistMaybe mAuthId
    isApplicant <- isCourseApplicant tid ssh csh uid
    guard isApplicant
    return Authorized

  r -> $unsupportedAuthPredicate AuthApplicant r
  where
    isCourseApplicant tid ssh csh uid = lift . $cachedHereBinary (uid, tid, ssh, csh) . E.selectExists . E.from $ \(course `E.InnerJoin` courseApplication) -> do
      E.on $ course E.^. CourseId E.==. courseApplication E.^. CourseApplicationCourse
      E.where_ $ courseApplication E.^. CourseApplicationUser E.==. E.val uid
           E.&&. course E.^. CourseTerm      E.==. E.val tid
           E.&&. course E.^. CourseSchool    E.==. E.val ssh
           E.&&. course E.^. CourseShorthand E.==. E.val csh
tagAccessPredicate AuthCapacity = APDB $ \_ _ _ route _ -> case route of
  CExamR tid ssh csh examn (ERegisterOccR occn) -> maybeT (unauthorizedI MsgExamOccurrenceNoCapacity) $ do
    cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
    eid <- $cachedHereBinary (cid, examn) . MaybeT . getKeyBy $ UniqueExam cid examn
    Entity occId ExamOccurrence{..} <- $cachedHereBinary (eid, occn) . MaybeT . getBy $ UniqueExamOccurrence eid occn
    -- Nothing means unlimited size
    whenIsJust examOccurrenceCapacity $ \capacity -> do
      registered <- $cachedHereBinary occId . lift $ fromIntegral <$> count [ ExamRegistrationOccurrence ==. Just occId, ExamRegistrationExam ==. eid ]
      guard $ capacity > registered
    return Authorized
  CTutorialR tid ssh csh tutn _ -> maybeT (unauthorizedI MsgTutorialNoCapacity) $ do
    cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
    Entity tutId Tutorial{..} <- $cachedHereBinary (cid, tutn) . MaybeT . getBy $ UniqueTutorial cid tutn
    registered <- $cachedHereBinary tutId . lift $ count [ TutorialParticipantTutorial ==. tutId ]
    guard $ NTop tutorialCapacity > NTop (Just registered)
    return Authorized
  CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgCourseNoCapacity) $ do
    Entity cid Course{..} <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
    registered <- $cachedHereBinary cid . lift $ count [ CourseParticipantCourse ==. cid, CourseParticipantState ==. CourseParticipantActive ]
    guard $ NTop courseCapacity > NTop (Just registered)
    return Authorized
  r -> $unsupportedAuthPredicate AuthCapacity r
tagAccessPredicate AuthRegisterGroup = APDB $ \_ _ mAuthId route _ -> case route of
  CTutorialR tid ssh csh tutn _ -> maybeT (unauthorizedI MsgUnauthorizedTutorialRegisterGroup) $ do
    cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
    Entity _ Tutorial{..} <- $cachedHereBinary (cid, tutn) . MaybeT . getBy $ UniqueTutorial cid tutn
    case (tutorialRegGroup, mAuthId) of
      (Nothing, _) -> return Authorized
      (_, Nothing) -> return AuthenticationRequired
      (Just rGroup, Just uid) -> do
        hasOther <- $cachedHereBinary (uid, rGroup) . lift . E.selectExists . E.from $ \(tutorial `E.InnerJoin` participant) -> do
          E.on $ tutorial E.^. TutorialId E.==. participant E.^. TutorialParticipantTutorial
           E.&&. tutorial E.^. TutorialCourse E.==. E.val tutorialCourse
           E.&&. tutorial E.^. TutorialRegGroup E.==. E.just (E.val rGroup)
           E.&&. participant E.^. TutorialParticipantUser E.==. E.val uid
        guard $ not hasOther
        return Authorized
  r -> $unsupportedAuthPredicate AuthRegisterGroup r
tagAccessPredicate AuthEmpty = APDB $ \_ _ mAuthId route _ -> case route of
  EExamListR -> exceptT return return $ do
    authId <- maybeExceptT AuthenticationRequired $ return mAuthId
    hasExternalExams <- $cachedHereBinary authId . lift . E.selectExists . E.from $ \(eexam `E.InnerJoin` eexamStaff) -> do
      E.on $ eexam E.^. ExternalExamId E.==. eexamStaff E.^. ExternalExamStaffExam
      E.where_ $ eexamStaff E.^. ExternalExamStaffUser E.==. E.val authId
        E.||. E.exists (E.from $ \externalExamResult ->
            E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. eexam E.^. ExternalExamId
                  E.&&. externalExamResult E.^. ExternalExamResultUser E.==. E.val authId
          )
    guardMExceptT (not hasExternalExams) $ unauthorizedI MsgUnauthorizedExternalExamListNotEmpty
    return Authorized
  CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgCourseNotEmpty) $ do
    -- Entity cid Course{..} <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
    cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
    assertM_ (<= 0) . $cachedHereBinary cid . lift $ count [ CourseParticipantCourse ==. cid ]
    assertM_ not . $cachedHereBinary cid . lift $ E.selectExists . E.from $ \(sheet `E.InnerJoin` submission) -> do
      E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
      E.where_ $ sheet E.^. SheetCourse E.==. E.val cid
    return Authorized
  r -> $unsupportedAuthPredicate AuthEmpty r
tagAccessPredicate AuthMaterials = APDB $ \_ _ _ route _ -> case route of
  CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgUnfreeMaterials) $ do
    Entity _ Course{..} <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
    guard courseMaterialFree
    return Authorized
  r -> $unsupportedAuthPredicate AuthMaterials r
tagAccessPredicate AuthOwner = APDB $ \_ _ mAuthId route _ -> case route of
  CSubmissionR _ _ _ _ cID _ -> $cachedHereBinary (mAuthId, cID) . exceptT return return $ do
    sid <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSubmissionOwner) (const True :: CryptoIDError -> Bool) $ decrypt cID
    authId <- maybeExceptT AuthenticationRequired $ return mAuthId
    void . maybeMExceptT (unauthorizedI MsgUnauthorizedSubmissionOwner) . getBy $ UniqueSubmissionUser authId sid
    return Authorized
  r -> $unsupportedAuthPredicate AuthOwner r
tagAccessPredicate AuthPersonalisedSheetFiles = APDB $ \_ _ mAuthId route _ -> case route of
  CSheetR tid ssh csh shn _ -> $cachedHereBinary (mAuthId, tid, ssh, csh, shn) . exceptT return return $ do
    Entity shId Sheet{..} <- maybeTMExceptT (unauthorizedI MsgUnauthorizedSubmissionPersonalisedSheetFiles) $ do
      cid <- MaybeT . $cachedHereBinary (tid, ssh, csh) . getKeyBy $ TermSchoolCourseShort tid ssh csh
      MaybeT . $cachedHereBinary (cid, shn) . getBy $ CourseSheet cid shn
    if | sheetAllowNonPersonalisedSubmission -> return Authorized
       | otherwise -> do
           authId <- maybeExceptT AuthenticationRequired $ return mAuthId
           flip guardMExceptT (unauthorizedI MsgUnauthorizedSubmissionPersonalisedSheetFiles) <=< $cachedHereBinary (shId, authId) . lift $
             E.selectExists . E.from $ \psFile ->
               E.where_ $ psFile E.^. PersonalisedSheetFileSheet E.==. E.val shId
                    E.&&. psFile E.^. PersonalisedSheetFileUser  E.==. E.val authId
                    E.&&. E.not_ (E.isNothing $ psFile E.^. PersonalisedSheetFileContent) -- directories don't count
           return Authorized
  r -> $unsupportedAuthPredicate AuthPersonalisedSheetFiles r
tagAccessPredicate AuthRated = APDB $ \_ _ _ route _ -> case route of
  CSubmissionR _ _ _ _ cID _ -> $cachedHereBinary cID . maybeT (unauthorizedI MsgUnauthorizedSubmissionRated) $ do
    sid <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
    sub <- MaybeT $ get sid
    guard $ submissionRatingDone sub
    return Authorized
  r -> $unsupportedAuthPredicate AuthRated r
tagAccessPredicate AuthUserSubmissions = APDB $ \_ _ _ route _ -> case route of
  CSheetR tid ssh csh shn _ -> $cachedHereBinary (tid, ssh, csh, shn) . maybeT (unauthorizedI MsgUnauthorizedUserSubmission) $ do
    Entity cid _ <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
    Entity _ Sheet{ sheetSubmissionMode = SubmissionMode{..} } <- MaybeT . getBy $ CourseSheet cid shn
    guard $ is _Just submissionModeUser
    return Authorized
  r -> $unsupportedAuthPredicate AuthUserSubmissions r
tagAccessPredicate AuthCorrectorSubmissions = APDB $ \_ _ _ route _ -> case route of
  CSheetR tid ssh csh shn _ -> maybeT (unauthorizedI MsgUnauthorizedCorrectorSubmission) $ do
    Entity cid _ <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
    Entity _ Sheet{ sheetSubmissionMode = SubmissionMode{..} } <- $cachedHereBinary (cid, shn) . MaybeT . getBy $ CourseSheet cid shn
    guard submissionModeCorrector
    return Authorized
  r -> $unsupportedAuthPredicate AuthCorrectorSubmissions r
tagAccessPredicate AuthCorrectionAnonymous = APDB $ \_ _ _ route _ -> case route of
  CSheetR tid ssh csh shn _ -> maybeT (unauthorizedI MsgUnauthorizedCorrectionAnonymous) $ do
    Entity cid _ <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
    Entity _ Sheet{ sheetAnonymousCorrection } <- $cachedHereBinary (cid, shn) . MaybeT . getBy $ CourseSheet cid shn
    guard sheetAnonymousCorrection
    return Authorized
  r -> $unsupportedAuthPredicate AuthCorrectionAnonymous r
tagAccessPredicate AuthSelf = APDB $ \_ _ mAuthId route _ -> exceptT return return $ do
  referencedUser' <- case route of
    AdminUserR cID -> return $ Left cID
    AdminUserDeleteR cID -> return $ Left cID
    AdminHijackUserR cID -> return $ Left cID
    UserNotificationR cID -> return $ Left cID
    UserPasswordR cID -> return $ Left cID
    CourseR _ _ _ (CUserR cID) -> return $ Left cID
    CApplicationR _ _ _ cID _ -> do
      appId <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt cID
      CourseApplication{..} <- maybeMExceptT (unauthorizedI MsgUnauthorizedSelf) . $cachedHereBinary appId $ get appId
      return $ Right courseApplicationUser
    _other -> throwError =<< $unsupportedAuthPredicate AuthSelf route
  referencedUser <- case referencedUser' of
    Right uid -> return uid
    Left cID  -> catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt cID
  case mAuthId of
    Just uid
      | uid == referencedUser -> return Authorized
    Nothing -> return AuthenticationRequired
    _other  -> unauthorizedI MsgUnauthorizedSelf
tagAccessPredicate AuthIsLDAP = APDB $ \_ _ _ route _ -> exceptT return return $ do
  referencedUser <- case route of
    AdminUserR cID -> return cID
    AdminUserDeleteR cID -> return cID
    AdminHijackUserR cID -> return cID
    UserNotificationR cID -> return cID
    UserPasswordR cID -> return cID
    CourseR _ _ _ (CUserR cID) -> return cID
    _other -> throwError =<< $unsupportedAuthPredicate AuthIsLDAP route
  referencedUser' <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt referencedUser
  maybeTMExceptT (unauthorizedI MsgUnauthorizedLDAP) $ do
    User{..} <- MaybeT $ get referencedUser'
    guard $ userAuthentication == AuthLDAP
  return Authorized
tagAccessPredicate AuthIsPWHash = APDB $ \_ _ _ route _ -> exceptT return return $ do
  referencedUser <- case route of
    AdminUserR cID -> return cID
    AdminUserDeleteR cID -> return cID
    AdminHijackUserR cID -> return cID
    UserNotificationR cID -> return cID
    UserPasswordR cID -> return cID
    CourseR _ _ _ (CUserR cID) -> return cID
    _other -> throwError =<< $unsupportedAuthPredicate AuthIsPWHash route
  referencedUser' <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSelf) (const True :: CryptoIDError -> Bool) $ decrypt referencedUser
  maybeTMExceptT (unauthorizedI MsgUnauthorizedPWHash) $ do
    User{..} <- MaybeT $ get referencedUser'
    guard $ is _AuthPWHash userAuthentication
  return Authorized
tagAccessPredicate AuthAuthentication = APDB $ \_ _ mAuthId route _ -> case route of
  MessageR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageAuth) $ do
    smId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
    SystemMessage{..} <- $cachedHereBinary smId . MaybeT $ get smId
    let isAuthenticated = isJust mAuthId
    guard $ not systemMessageAuthenticatedOnly || isAuthenticated
    return Authorized
  MessageHideR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageAuth) $ do
    smId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
    SystemMessage{..} <- $cachedHereBinary smId . MaybeT $ get smId
    let isAuthenticated = isJust mAuthId
    guard $ not systemMessageAuthenticatedOnly || isAuthenticated
    return Authorized
  r -> $unsupportedAuthPredicate AuthAuthentication r
tagAccessPredicate AuthRead = APPure $ \_ _ isWrite -> do
  MsgRenderer mr <- ask
  return $ bool Authorized (Unauthorized $ mr MsgUnauthorizedWrite) isWrite
tagAccessPredicate AuthWrite = APPure $ \_ _ isWrite -> do
  MsgRenderer mr <- ask
  return $ bool (Unauthorized $ mr MsgUnauthorized) Authorized isWrite

runTACont :: forall m. MonadAP m
          => (forall m'. MonadAP m' => AuthTagsEval m')
          -> AuthDNF -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m Bool
runTACont cont dnf mAuthId route isWrite = is _Authorized <$> evalWriterT (cont dnf mAuthId route isWrite)


authTagSpecificity :: AuthTag -> AuthTag -> Ordering
-- ^ Heuristic for which `AuthTag`s to evaluate first
authTagSpecificity = comparing $ NTop . flip findIndex eqClasses . elem
  where
    eqClasses :: [[AuthTag]]
    -- ^ Constructors of `AuthTag` ordered (increasing) by execution order
    eqClasses =
      [ [ AuthFree, AuthDeprecated, AuthDevelopment ] -- Route wide
      , [ AuthRead, AuthWrite, AuthToken ] -- Request wide
      , [ AuthAdmin ] -- Site wide
      , [ AuthLecturer, AuthCourseRegistered, AuthParticipant, AuthCourseTime, AuthTime, AuthMaterials, AuthUserSubmissions, AuthCorrectorSubmissions, AuthCapacity, AuthEmpty ] ++ [ AuthSelf, AuthNoEscalation ] ++ [ AuthAuthentication ] -- Course/User/SystemMessage wide
      , [ AuthCorrector ] ++ [ AuthTutor ] ++ [ AuthTutorialRegistered, AuthRegisterGroup ] -- Tutorial/Material/Sheet wide
      , [ AuthOwner, AuthRated ] -- Submission wide
      ]

routeAuthTags :: Route UniWorX -> Either InvalidAuthTag AuthDNF
-- ^ DNF up to entailment, see `predDNFEntail`
routeAuthTags = fmap predDNFEntail . ofoldM parse defaultAuthDNF . routeAttrs
  where
    parse :: AuthDNF -> Text -> Either InvalidAuthTag AuthDNF
    parse prev t = case fromNullable . Set.fromList =<< mapM fromPathPiece (Text.splitOn "AND" t) of
      Just t' -> Right . predDNFOr prev . PredDNF $ Set.singleton t'
      Nothing -> Left $ InvalidAuthTag t

broadenRoute :: AuthTag -> Route UniWorX -> Route UniWorX
broadenRoute aTag route = case (aTag, route) of
  (AuthAdmin, CourseR tid ssh csh _) -> CourseR tid ssh csh CShowR
  (AuthAdmin, AllocationR tid ssh ash _) -> AllocationR tid ssh ash AShowR
  (AuthAdmin, SchoolR ssh _) -> SchoolR ssh SchoolEditR
  (AuthAdmin, _) -> NewsR

  (AuthStudent, _) -> NewsR

  (AuthExamOffice, CExamR tid ssh csh examn _) -> CExamR tid ssh csh examn EShowR
  (AuthExamOffice, EExamR tid ssh coursen examn _) -> EExamR tid ssh coursen examn EEShowR
  (AuthExamOffice, CourseR _ ssh _ _) -> SchoolR ssh SchoolEditR
  (AuthExamOffice, SchoolR ssh _) -> SchoolR ssh SchoolEditR
  (AuthExamOffice, _) -> NewsR

  (AuthLecturer, CourseR tid ssh csh _) -> CourseR tid ssh csh CShowR
  (AuthLecturer, AllocationR tid ssh ash _) -> AllocationR tid ssh ash AShowR
  (AuthLecturer, EExamR tid ssh coursen examn _) -> EExamR tid ssh coursen examn EEShowR
  (AuthLecturer, _) -> NewsR

  _other -> route

evalAuthTags :: forall ctx m. (HasCallStack, Binary ctx, MonadAP m) => ctx -> AuthTagActive -> (forall m'. MonadAP m' => AuthTagsEval m') -> AuthTagsEval m
-- ^ `tell`s disabled predicates, identified as pivots
evalAuthTags ctx authActive@AuthTagActive{..} cont (map (Set.toList . toNullable) . Set.toList . dnfTerms -> authDNF') mAuthId route isWrite
  = do
      mr <- getMsgRenderer
      let
        contCtx = toStrict $ Binary.encode (ctx, authActive)
        authVarSpecificity = authTagSpecificity `on` plVar
        authDNF = sortBy (authVarSpecificity `on` maximumBy authVarSpecificity . impureNonNull) $ map (sortBy authVarSpecificity) authDNF'

        authTagIsInactive = not . authTagIsActive

        evalAuthTag :: AuthTag -> WriterT (Set AuthTag) m AuthResult
        evalAuthTag authTag = lift . ($runCachedMemoT :: CachedMemoT (ctx, AuthTag, Maybe UserId, Route UniWorX, Bool) AuthResult m _ -> m _) $ for5 memo (const evalAccessPred') ctx authTag mAuthId route'' isWrite
          where
            route'' = broadenRoute authTag route
            evalAccessPred' authTag' mAuthId' route' isWrite' = lift $ do
              $logDebugS "evalAccessPred" $ tshow (authTag', mAuthId', route', isWrite')
              observeAuthTagEvaluation authTag' (classifyHandler route') $ do
                res <- evalAccessPred (tagAccessPredicate authTag') contCtx cont mAuthId' route' isWrite'
                return . (res, ) $ case res of
                  Authorized             -> OutcomeAuthorized
                  Unauthorized _         -> OutcomeUnauthorized
                  AuthenticationRequired -> OutcomeAuthenticationRequired

        evalAuthLiteral :: AuthLiteral -> WriterT (Set AuthTag) m AuthResult
        evalAuthLiteral PLVariable{..} = evalAuthTag plVar
        evalAuthLiteral PLNegated{..} = notAR mr plVar <$> evalAuthTag plVar

        orAR', andAR' :: forall m'. Monad m' => m' AuthResult -> m' AuthResult -> m' AuthResult
        orAR' = shortCircuitM (is _Authorized) (orAR mr)
        andAR' = shortCircuitM (is _Unauthorized) (andAR mr)

        evalDNF :: [[AuthLiteral]] -> WriterT (Set AuthTag) m AuthResult
        evalDNF = maybe (return $ falseAR mr) (ofoldr1 orAR') . fromNullable . map evalConj
          where
            evalConj = maybe (return $ trueAR mr) (ofoldr1 andAR') . fromNullable . map evalAuthLiteral

      $logDebugS "evalAuthTags" . tshow . (route, isWrite, ) $ map (map $ id &&& authTagIsActive . plVar) authDNF

      result <- evalDNF $ filter (all $ authTagIsActive . plVar) authDNF

      unless (is _Authorized result) . forM_ (filter (any $ authTagIsInactive . plVar) authDNF) $ \conj ->
        whenM (allM conj (\aTag -> (return . not . authTagIsActive $ plVar aTag) `or2M` (not . is _Unauthorized <$> evalAuthLiteral aTag))) $ do
          let pivots = filter (authTagIsInactive . plVar) conj
          whenM (allM pivots $ fmap (is _Authorized) . evalAuthLiteral) $ do
            let pivots' = plVar <$> pivots
            $logDebugS "evalAuthTags" [st|Recording pivots: #{tshow pivots'}|]
            tell $ Set.fromList pivots'

      return result

evalAccessWithFor :: (HasCallStack, MonadThrow m, MonadAP m) => [(AuthTag, Bool)] -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m AuthResult
evalAccessWithFor assumptions mAuthId route isWrite = do
  isSelf <- (== mAuthId) <$> liftHandler defaultMaybeAuthId
  tagActive <- if
    | isSelf    -> fromMaybe def <$> lookupSessionJson SessionActiveAuthTags
    | otherwise -> return . AuthTagActive $ const True
  dnf <- throwLeft $ routeAuthTags route
  let adjDNF = ala Endo foldMap (map ((=<<) . uncurry dnfAssumeValue) assumptions) . Just
      evalAdj :: forall m'. MonadAP m' => AuthTagsEval m'
      evalAdj (adjDNF -> dnf') mAuthId' route' isWrite' = case dnf' of
        Nothing    -> return Authorized
        Just dnf'' -> evalAuthTags ('evalAccessWithFor, assumptions) tagActive evalAdj dnf'' mAuthId' route' isWrite'
   in do
    (result, deactivated) <- runWriterT $ evalAdj dnf mAuthId route isWrite
    when isSelf $
      tellSessionJson SessionInactiveAuthTags deactivated
    return result

evalAccessFor :: (HasCallStack, MonadThrow m, MonadAP m) => Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m AuthResult
evalAccessFor = evalAccessWithFor []

evalAccessForDB :: (HasCallStack, MonadThrow m, MonadAP m, BackendCompatible SqlReadBackend backend) => Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> ReaderT backend m AuthResult
evalAccessForDB = evalAccessFor

evalAccessWith :: (HasCallStack, MonadThrow m, MonadAP m) => [(AuthTag, Bool)] -> Route UniWorX -> Bool -> m AuthResult
evalAccessWith assumptions route isWrite = do
  mAuthId <- liftHandler maybeAuthId
  evalAccessWithFor assumptions mAuthId route isWrite
  
evalAccessWithDB :: (HasCallStack, MonadThrow m, MonadAP m, BackendCompatible SqlReadBackend backend) => [(AuthTag, Bool)] -> Route UniWorX -> Bool -> ReaderT backend m AuthResult
evalAccessWithDB = evalAccessWith

evalAccess :: (HasCallStack, MonadThrow m, MonadAP m) => Route UniWorX -> Bool -> m AuthResult
evalAccess = evalAccessWith []

evalAccessDB :: (HasCallStack, MonadThrow m, MonadAP m, BackendCompatible SqlReadBackend backend) => Route UniWorX -> Bool -> ReaderT backend m AuthResult
evalAccessDB = evalAccess

-- | Check whether the current user is authorized by `evalAccess` for the given route
--   Convenience function for a commonly used code fragment
hasAccessTo :: (HasCallStack, MonadThrow m, MonadAP m) => Route UniWorX -> Bool -> m Bool
hasAccessTo route isWrite = (== Authorized) <$> evalAccess route isWrite

-- | Check whether the current user is authorized by `evalAccess` to read from the given route
--   Convenience function for a commonly used code fragment
hasReadAccessTo :: (HasCallStack, MonadThrow m, MonadAP m) => Route UniWorX -> m Bool
hasReadAccessTo = flip hasAccessTo False

-- | Check whether the current user is authorized by `evalAccess` to rwrite to the given route
--   Convenience function for a commonly used code fragment
hasWriteAccessTo :: (HasCallStack, MonadThrow m, MonadAP m) => Route UniWorX -> m Bool
hasWriteAccessTo = flip hasAccessTo True

wouldHaveAccessTo :: (HasCallStack, MonadThrow m, MonadAP m)
                  => [(AuthTag, Bool)] -- ^ Assumptions
                  -> Route UniWorX
                  -> Bool
                  -> m Bool
wouldHaveAccessTo assumptions route isWrite = (== Authorized) <$> evalAccessWith assumptions route isWrite

wouldHaveReadAccessTo, wouldHaveWriteAccessTo
  :: (HasCallStack, MonadThrow m, MonadAP m)
  => [(AuthTag, Bool)] -- ^ Assumptions
  -> Route UniWorX
  -> m Bool
wouldHaveReadAccessTo assumptions route = wouldHaveAccessTo assumptions route False
wouldHaveWriteAccessTo assumptions route = wouldHaveAccessTo assumptions route True

wouldHaveReadAccessToIff, wouldHaveWriteAccessToIff
  :: (HasCallStack, MonadThrow m, MonadAP m)
  => [(AuthTag, Bool)] -- ^ Assumptions
  -> Route UniWorX
  -> m Bool
wouldHaveReadAccessToIff assumptions route = and2M (not <$> hasReadAccessTo route) $ wouldHaveReadAccessTo assumptions route
wouldHaveWriteAccessToIff assumptions route = and2M (not <$> hasWriteAccessTo route) $ wouldHaveWriteAccessTo assumptions route


authoritiveApproot :: Route UniWorX -> ApprootScope
authoritiveApproot = \case
  CourseR _ _ _ (MaterialR _ (MFileR _))                      -> ApprootUserGenerated
  CourseR _ _ _ (MaterialR _ MArchiveR)                       -> ApprootUserGenerated
  CourseR _ _ _ (SheetR _ (SFileR _ _))                       -> ApprootUserGenerated
  CourseR _ _ _ (SheetR _ (SZipR _))                          -> ApprootUserGenerated
  CourseR _ _ _ (SheetR _ (SubmissionR _ (SubDownloadR _ _))) -> ApprootUserGenerated
  CourseR _ _ _ (SheetR _ (SubmissionR _ (SubArchiveR _)))    -> ApprootUserGenerated
  CourseR _ _ _ (CourseNewsR _ (CNFileR _))                   -> ApprootUserGenerated
  CourseR _ _ _ (CourseNewsR _ CNArchiveR)                    -> ApprootUserGenerated
  CourseR _ _ _ CRegisterTemplateR                            -> ApprootUserGenerated
  CourseR _ _ _ CAppsFilesR                                   -> ApprootUserGenerated
  CourseR _ _ _ (CourseApplicationR _ CAFilesR)               -> ApprootUserGenerated
  _other -> ApprootDefault
