По-разному родил ReaderT? - PullRequest
2 голосов
/ 29 июня 2019

С риском того, что это станет XY-проблемой , возможно ли иметь ReaderT в другой среде? Я пытаюсь что-то вроде ...

type AppM (perms :: [*]) = ReaderT (perms :: [*]) IO

... но компилятор жалуется ...

Expected a type, but ‘(perms :: [*])’ has kind ‘[*]’

... предположительно потому что ReaderT определяется как ...

newtype ReaderT r (m :: k -> *) (a :: k) = ReaderT {runReaderT :: r -> m a}

... где r имеет вид *

Я пытаюсь отслеживать разрешения / роли на уровне типа, и моя конечная цель - написать такие функции, как ...

ensurePermission :: (p :: Permission) -> AppM (p :. ps) ()

... где каждый вызов ensurePermission добавляет / добавляет новое разрешение в список разрешений монады (на уровне типа).

Редактировать

Я попробовал следующее, и оно, кажется, компилируется, но я не уверен, что происходит Концептуально это не perms все еще вида [*]. Как этот фрагмент приемлем для компилятора, а оригинальный - нет?

data HList (l :: [*]) where
  HNil :: HList '[]
  HCons :: e -> HList l -> HList (e ': l)

type AppM (perms :: [*]) = ReaderT (HList perms) IO

Редактировать # 2

Я пытался развить свой фрагмент кода, чтобы он больше соответствовал моей конечной цели, но я снова застрял с другой «доброй» проблемой:

Компилятор не принимает следующий код:

{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}

data Permission = PermissionA
                | PermissionB

$(genSingletons [''Permission])

data PList (perms :: [Permission]) where
  PNil :: PList '[]
  PCons :: p -> PList perms -> PList (p ': perms)

--     • Expected kind ‘[Permission]’, but ‘p : perms’ has kind ‘[*]’
--     • In the first argument of ‘PList’, namely ‘(p : perms)’
--       In the type ‘PList (p : perms)’
--       In the definition of data constructor ‘PCons’
--    |
-- 26 |   PCons :: p -> PList perms -> PList (p ': perms)
--    |                                       ^^^^^^^^^^

Также не принимаются следующие варианты ...

data PList (perms :: [Permission]) where
  PNil :: PList '[]
  PCons :: (p :: Permission) -> PList perms -> PList (p ': perms)


--     • Expected a type, but ‘(p :: Permission)’ has kind ‘Permission’
--     • In the type ‘(p :: Permission)’
--       In the definition of data constructor ‘PCons’
--       In the data declaration for ‘PList’
--    |
-- 26 |   PCons :: (p :: Permission) -> PList perms -> PList (p ': perms)
--    |            ^^^^^^^^^^^^^^^^^

Ответы [ 2 ]

4 голосов
/ 29 июня 2019

Да, я думаю, что у нас здесь проблема XY, поэтому давайте сделаем шаг назад.

A Reader - это монада для переноса значения , которое удобно читать. У вас нет значения - у вас есть список разрешений, которые вы хотите применить на уровне типа - поэтому я не думаю, что вам нужен или нужен читатель, или гетерогенный список, или что-то еще в этом роде.

Вместо этого предоставляется список логических разрешений:

data Permission = PermissionA | PermissionB deriving (Show)

Вы хотите определить монаду, параметризованную на уровне типа, со списком предоставленных ей разрешений. Обертка нового типа вокруг вашей основной монады IO подойдет:

{-# LANGUAGE DataKinds, KindSignatures, GeneralizedNewtypeDeriving #-}
newtype M (ps :: [Permission]) a = M (IO a) deriving (Functor, Applicative, Monad)

Вы также захотите, чтобы функция типа (семейство типов AKA) определяла, есть ли разрешение в списке разрешений:

{-# LANGUAGE TypeFamilies, TypeOperators #-}
type family Allowed (p :: Permission) ps where
  Allowed p '[] = False
  Allowed p (p:ps) = True
  Allowed p (q:ps) = Allowed p ps

Теперь, если вы хотите писать функции, требующие определенных разрешений, вы пишете что-то вроде:

deleteA :: (Allowed PermissionA ps ~ True) => M ps ()
deleteA = M $ print "Deleted A"

readB :: (Allowed PermissionB ps ~ True) => M ps ()
readB = M $ print "Read B"

copyBtoA :: ( Allowed PermissionA ps ~ True
            , Allowed PermissionB ps ~ True) => M ps ()
copyBtoA = M $ print "Copied B to A"

Чтобы запустить действие M, мы вводим функцию, которая запускает действие без прав доступа:

-- runM with no permissions
runM :: M '[] a -> IO a
runM (M act) = act

Обратите внимание, что если вы попробуете runM readB, вы получите ошибку типа (не может соответствовать False с True - не самое большое сообщение об ошибке, но ...).

Для предоставления разрешений мы вводим функции:

-- grant permissions
grantA :: M (PermissionA:ps) a -> M ps a
grantA (M act) = M act
grantB :: M (PermissionB:ps) a -> M ps a
grantB (M act) = M act

Эти функции по сути являются тождественными функциями на уровне терминов - они просто разворачивают и переворачивают конструктор M. Однако их работа на уровне типа заключается в добавлении разрешения к их входному аргументу. Это означает, что:

runM $ grantB $ readB

теперь проверки типов. Так же:

runM $ grantA . grantB $ readB
runM $ grantB . grantA $ readB
runM $ grantB . grantA . grantB $ readB
etc.

Тогда вы можете писать программы вроде:

program :: IO ()
program = runM $ do
  grantA $ do
    deleteA
    grantB $ do
      readB
      copyBtoA

при отклонении таких программ, как:

program1 :: IO ()
program1 = runM $ do
  grantA $ do
    deleteA
    grantB $ do
      readB
    copyBtoA    -- error, needs PermissionB

Эта инфраструктура может быть немного уродливой, но она должна быть все, что вам нужно для проверки разрешений на основе типов при компиляции.

Возможно, попробуйте эту версию немного и посмотрите, соответствует ли она вашим потребностям. Полный код:

{-# LANGUAGE DataKinds, KindSignatures, GeneralizedNewtypeDeriving,
             TypeFamilies, TypeOperators #-}

data Permission = PermissionA | PermissionB deriving (Show)

newtype M (ps :: [Permission]) a = M (IO a) deriving (Functor, Applicative, Monad)

type family Allowed (p :: Permission) ps where
  Allowed p '[] = False
  Allowed p (p:ps) = True
  Allowed p (q:ps) = Allowed p ps

-- runM with no permissions
runM :: M '[] a -> IO a
runM (M act) = act

-- grant permissions
grantA :: M (PermissionA:ps) a -> M ps a
grantA (M act) = M act
grantB :: M (PermissionB:ps) a -> M ps a
grantB (M act) = M act

deleteA :: (Allowed PermissionA ps ~ True) => M ps ()
deleteA = M $ print "Deleted A"

readB :: (Allowed PermissionB ps ~ True) => M ps ()
readB = M $ print "Read B"

copyBtoA :: ( Allowed PermissionA ps ~ True
            , Allowed PermissionB ps ~ True) => M ps ()
copyBtoA = M $ print "Copied B to A"

program :: IO ()
program = runM $ do
  grantA $ do
    deleteA
    grantB $ do
      readB
      copyBtoA

Два дополнительных примечания на основе комментария @ dfeuer. Во-первых, это напомнило мне, что grantA и grantB могут быть одинаково хорошо написаны с использованием «безопасной» функции coerce из Data.Coerce следующим образом. Нет разницы в коде, сгенерированном между этой версией и версией выше, так что это дело вкуса:

import Data.Coerce

-- grant permissions
grantA :: M (PermissionA:ps) a -> M ps a
grantA = coerce
grantB :: M (PermissionB:ps) a -> M ps a
grantB = coerce

Во-вторых, @dfeuer говорит о том, что здесь нет четкого разделения между базой доверенного кода для управления разрешениями и «остальной частью» кода, который опирается на систему типов для обеспечения работы системы разрешений. Например, сопоставление с образцом в конструкторе M опасно по своей природе, поскольку вы можете извлечь IO a из одного контекста разрешения и реконструировать его в другом. (Это в основном то, что grantA и grantB делают для безусловного повышения привилегий.) Если вы делаете это «случайно» за пределами базы надежного кода, вы можете обойти систему разрешений. Во многих приложениях это не имеет большого значения.

Однако, если вы пытаетесь доказать безопасность системы, вам может потребоваться небольшая база надежного кода, которая работает с опасным конструктором M и экспортирует только «безопасный» API, который обеспечивает безопасность через систему типов. В этом случае у вас будет модуль, который экспортирует тип M, но не его конструктор M(..). Вместо этого вы должны экспортировать умные конструкторы для создания M действий с соответствующими разрешениями.

Кроме того, по непонятным техническим причинам, даже без экспорта конструктора M, все еще будет возможно, чтобы «ненадежный» код приводил между различными контекстами разрешений:

stealPermission :: M (PermissionA:ps) a -> M ps a
stealPermission = coerce

потому что первый параметр конструктора типа M имеет так называемую «роль», которая по умолчанию «фантомная» вместо «номинальная». Если вы переопределите это:

{-# LANGUAGE RoleAnnotations #-}
type role M nominal _

затем coerce можно использовать только в том случае, если конструктор находится в области видимости, которая закрывает эту лазейку. Ненадежный код все еще может использовать unsafeCoerce, но есть механизмы (Google для «Safe Haskell»), чтобы предотвратить это.

2 голосов
/ 30 июня 2019

В отдельной Gist вы прокомментировали:

@K.A.Buhr, вау! Спасибо за такой подробный ответ. Вы правы, что это проблема XY, и вы в значительной степени прибили реальную проблему, которую я пытаюсь решить. Еще одна важная часть контекста заключается в том, что в какой-то момент эти разрешения на уровне типов должны быть «реализованы» на уровне значений. Это связано с тем, что последняя проверка сопоставлена ​​с разрешениями, предоставленными текущему вошедшему в систему пользователю, которые хранятся в БД.

Учитывая это, я планирую иметь две "общие" функции, скажем:

requiredPermission :: (RequiredPermission p ps) => Proxy p -> AppM ps ()
optionalPermission :: (OptionalPermission p ps) => Proxy p -> AppM ps ()

Вот разница:

  • requiredPermission просто добавит разрешение в список уровня типа и будет проверено при вызове runAppM. Если текущий пользователь не имеет ВСЕХ требуемых разрешений, runAppM немедленно выдаст ошибку 401 в пользовательский интерфейс.
  • С другой стороны, optionalPermission извлечет пользователя из среды Reader, проверит разрешение и вернет True / False. runAppM ничего не сделает с OptionalPermissions. Это будет в тех случаях, когда отсутствие разрешения НЕ должно проваливать все действие, но пропустить определенный шаг в действии.

Учитывая этот контекст, я не уверен, что в конечном итоге я получу такие функции, как grantA или grantB. «Развертывание» ВСЕХ RequestPermissions в конструкторе AppM будет выполнено runAppM, что также гарантирует, что у текущего вошедшего в систему пользователя действительно есть эти разрешения.

Обратите внимание, что существует несколько способов "переопределить" типы. Например, следующая программа - с помощью хитрого трюка с черной магией - может преобразовать тип среды выполнения без использования прокси или синглетонов!

main = do
  putStr "Enter \"Int\" or \"String\": "
  s <- getLine
  putStrLn $ case s of "Int" ->    "Here is an integer: " ++ show (42 :: Int)
                       "String" -> "Here is a string: " ++ show ("hello" :: String)

Аналогично, следующий вариант grantA позволяет поднять пользовательские разрешения, известные только во время выполнения, до уровня типа:

whenA :: M (PermissionA:ps) () -> M ps ()
whenA act = do
  perms <- asks userPermissions  -- get perms from environment
  if PermissionA `elem` perms
    then act
    else notAuthenticated

Синглтоны могут использоваться здесь, чтобы избежать шаблонов для различных разрешений и улучшить безопасность типов в этом доверенном куске кода (то есть, чтобы два вхождения PermissionA были вынуждены совпадать). Точно так же виды ограничений могут сохранять 5 или 6 символов на проверку разрешения. Тем не менее, ни одно из этих улучшений не является необходимым, и они могут добавить существенную сложность, которой следует избегать, если это вообще возможно до тех пор, пока после не получится работающий прототип. Другими словами, элегантный код, который не работает, не так уж и элегантен.

В этом духе, вот как я мог бы адаптировать свое исходное решение для поддержки набора «требуемых» разрешений, которые должны быть удовлетворены в определенных «точках входа» (например, в определенных перенаправленных веб-запросах), и для выполнения проверки разрешений во время выполнения к базе данных пользователей.

Во-первых, у нас есть набор разрешений:

data Permission
  = ReadP            -- read content
  | MetaP            -- view (private) metadata
  | WriteP           -- write content
  | AdminP           -- all permissions
  deriving (Show, Eq)

и пользовательские данные:

type User = String
userDB :: [(User, [Permission])]
userDB
  = [ ("alice", [ReadP, WriteP])
    , ("bob",   [ReadP])
    , ("carl",  [AdminP])
    ]

, а также среда, включающая пользовательские права и все, что вы хотите носить с собой в ридере:

data Env = Env
  { uperms :: [Permission]   -- user's actual permissions
  , user :: String           -- other Env stuff
  } deriving (Show)

Мы также хотим, чтобы функции на уровне типа и термина проверяли списки разрешений:

type family Allowed (p :: Permission) ps where
  Allowed p (AdminP:ps) = True   -- admins can do anything
  Allowed p '[] = False
  Allowed p (p:ps) = True
  Allowed p (q:ps) = Allowed p ps
allowed :: Permission -> [Permission] -> Bool
allowed p (AdminP:ps) = True
allowed p (q:ps) | p == q = True
                 | otherwise = allowed p ps
allowed p [] = False

(Да, вы можете использовать библиотеку singletons для одновременного определения обеих функций, но пока давайте сделаем это без синглетонов.)

Как и раньше, у нас будет монада, которая содержит список разрешений. Вы можете думать об этом как о списке разрешений, которые были проверены и проверены на данный момент в коде. Мы сделаем это монадным трансформатором для общего m с компонентом ReaderT Env:

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
newtype AppT (perms :: [Permission]) m a = AppT (ReaderT Env m a)
  deriving (Functor, Applicative, Monad, MonadReader Env, MonadIO)

Теперь мы можем определить действия в этой монаде, которые формируют строительные блоки для нашего приложения:

readPage :: (Allowed ReadP perms ~ True, MonadIO m) => Int -> AppT perms m ()
readPage n = say $ "Read page " ++ show n

metaPage :: (Allowed ReadP perms ~ True, MonadIO m) => Int -> AppT perms m ()
metaPage n = say $ "Secret metadata " ++ show (n^2)

editPage :: (Allowed ReadP perms ~ True, Allowed WriteP perms ~ True, MonadIO m) => Int -> AppT perms m ()
editPage n = say $ "Edit page " ++ show n

say :: MonadIO m => String -> m ()
say = liftIO . putStrLn

В каждом случае действие разрешено в любом контексте, где список проверенных и проверенных разрешений включает необходимые разрешения, перечисленные в сигнатуре типа. (Да, виды ограничений здесь будут работать нормально, но давайте будем простыми.)

Из них мы можем построить более сложные действия, как мы это делали в моем другом ответе:

readPageWithMeta :: ( Allowed 'ReadP perms ~ 'True, Allowed 'MetaP perms ~ 'True
    , MonadIO m) => Int -> AppT perms m ()
readPageWithMeta n = do
  readPage n
  metaPage n

Обратите внимание, что GHC может автоматически определить сигнатуру этого типа, определив, что требуются разрешения ReadP и MetaP. Если бы мы хотели сделать разрешение MetaP необязательным, мы могли бы написать:

readPageWithOptionalMeta :: ( Allowed 'ReadP perms ~ 'True
    , MonadIO m) => Int -> AppT perms m ()
readPageWithOptionalMeta n = do
  readPage n
  whenMeta $ metaPage n

, где whenMeta допускает необязательные действия в зависимости от доступных разрешений. (См. Ниже.) Опять же, эта подпись может быть выведена автоматически.

Пока что мы допустили необязательные разрешения, мы явно не имели дело с «необходимыми» разрешениями. Они будут определены в точках входа , которые будут определены с использованием отдельной монады:

newtype EntryT' (reqP :: [Permission]) (checkedP :: [Permission]) m a
  = EntryT (ReaderT Env m a)
  deriving (Functor, Applicative, Monad, MonadReader Env, MonadIO)
type EntryT reqP = EntryT' reqP reqP

Это требует некоторого объяснения. EntryT' (с галочкой) имеет два списка разрешений. Первый представляет собой полный список необходимых разрешений для точки входа и имеет фиксированное значение для каждой конкретной точки входа. Вторым является подмножество тех разрешений, которые были «проверены» (в статическом смысле, что имеется вызов функции для проверки и подтверждения того, что у пользователя есть необходимые разрешения). Он будет построен из пустого списка в полный список необходимых разрешений, когда мы определим точки входа. Мы будем использовать его в качестве механизма на уровне типов, чтобы гарантировать наличие правильного набора вызовов функций проверки прав доступа. EntryT (без галочки) имеет (статически) проверенные разрешения, равные его требуемым разрешениям, и поэтому мы знаем, что его можно безопасно запускать (в отношении динамически определенного набора разрешений конкретного пользователя, который будет проверяться в соответствии с гарантией типа).

runEntryT :: MonadIO m => User -> EntryT req m () -> m ()
runEntryT u (EntryT act)
  = case lookup u userDB of
      Nothing   -> say $ "error 401: no such user '" ++ u ++ "'"
      Just perms -> runReaderT act (Env perms u)

Чтобы определить точку входа, мы будем использовать что-то вроде этого:

entryReadPage :: MonadIO m => Int -> EntryT '[ReadP] m ()
entryReadPage n = _somethingspecial_ $ do
  readPage n
  whenMeta $ metaPage n

Обратите внимание, что у нас есть блок do, построенный из AppT строительных блоков. Фактически, это эквивалентно readPageWithOptionalMeta выше и имеет тип:

(Allowed 'ReadP perms ~ 'True, MonadIO m) => Int -> AppT perms m ()

Здесь _somethingspecial_ необходимо адаптировать это AppT (чей список разрешений требует проверки и проверки ReadP до его запуска) для точки входа, чьи списки требуемых и (статически) проверенных разрешений [ReadP]. Мы сделаем это, используя набор функций для проверки фактических прав доступа во время выполнения:

requireRead :: MonadIO m => EntryT' r c m () -> EntryT' r (ReadP:c) m ()
requireRead = unsafeRequire ReadP
requireWrite :: MonadIO m => EntryT' r c m () -> EntryT' r (WriteP:c) m ()
requireWrite = unsafeRequire WriteP
-- plus functions for the rest of the permissions

все определено в терминах:

unsafeRequire :: MonadIO m => Permission -> EntryT' r c m () -> EntryT' r c' m ()
unsafeRequire p act = do
  ps <- asks uperms
  if allowed p ps
    then coerce act
    else say $ "error 403: requires permission " ++ show p

Теперь, когда мы пишем:

entryReadPage :: MonadIO m => Int -> EntryT '[ReadP] m ()
entryReadPage n = requireRead . _ $ do
  readPage n
  whenMeta $ metaPage n

внешний тип является правильным, отражая тот факт, что список функций requireXXX соответствует списку необходимых разрешений в сигнатуре типа. Оставшееся отверстие имеет тип:

AppT perms0 m0 () -> EntryT' '[ReadP] '[] m ()

Из-за того, как мы структурировали нашу проверку разрешений, это особый случай безопасного преобразования:

toRunAppT :: MonadIO m => AppT r m a -> EntryT' r '[] m a
toRunAppT = coerce

Другими словами, мы можем написать наше окончательное определение точки входа, используя довольно хороший синтаксис, который буквально говорит, что нам «требуется Read для запуска этого AppT»:

entryReadPage :: MonadIO m => Int -> EntryT '[ReadP] m ()
entryReadPage n = requireRead . toRunAppT $ do
  readPage n
  whenMeta $ metaPage n

и аналогично:

entryEditPage :: MonadIO m => Int -> EntryT '[ReadP, WriteP] m ()
entryEditPage n = requireRead . requireWrite . toRunAppT $ do
  editPage n
  whenMeta $ metaPage n

Обратите внимание, что список необходимых разрешений явно включен в тип точки входа, и составной список функций requireXXX, которые выполняют проверку этих разрешений во время выполнения, должен точно совпадать с теми же разрешениями в том же порядке, чтобы он проверка типа.

Последней частью головоломки является реализация whenMeta, которая выполняет проверку разрешений во время выполнения и выполняет необязательное действие, если разрешение доступно.

whenMeta :: Monad m => AppT (MetaP:perms) m () -> AppT perms m ()
whenMeta = unsafeWhen MetaP
-- and similar functions for other permissions

unsafeWhen :: Monad m => Permission -> AppT perms m () -> AppT perms' m ()
unsafeWhen p act = do
  ps <- asks uperms
  if allowed p ps
    then coerce act
    else return ()

Вот полная программа с тестовым харнасом. Вы можете видеть, что:

Username/Req (e.g., "alice Read 5"): alice Read 5    -- Alice...
Read page 5
Username/Req (e.g., "alice Read 5"): bob Read 5      -- and Bob can read.
Read page 5
Username/Req (e.g., "alice Read 5"): carl Read 5     -- Carl gets the metadata, too
Read page 5
Secret metadata 25
Username/Req (e.g., "alice Read 5"): bob Edit 3      -- Bob can't edit...
error 403: requires permission WriteP
Username/Req (e.g., "alice Read 5"): alice Edit 3    -- but Alice can.
Edit page 3
Username/Req (e.g., "alice Read 5"):

Источник:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

module Realistic where

import Control.Monad.Reader
import Data.Coerce

-- |Set of permissions
data Permission
  = ReadP            -- read content
  | MetaP            -- view (private) metadata
  | WriteP           -- write content
  | AdminP           -- all permissions
  deriving (Show, Eq)

type User = String
-- |User database
userDB :: [(User, [Permission])]
userDB
  = [ ("alice", [ReadP, WriteP])
    , ("bob",   [ReadP])
    , ("carl",  [AdminP])
    ]

-- |Environment with 'uperms' and whatever else is needed
data Env = Env
  { uperms :: [Permission]   -- user's actual permissions
  , user :: String           -- other Env stuff
  } deriving (Show)

-- |Check for permission in type-level and term-level lists
type family Allowed (p :: Permission) ps where
  Allowed p (AdminP:ps) = True   -- admins can do anything
  Allowed p '[] = False
  Allowed p (p:ps) = True
  Allowed p (q:ps) = Allowed p ps
allowed :: Permission -> [Permission] -> Bool
allowed p (AdminP:ps) = True
allowed p (q:ps) | p == q = True
                 | otherwise = allowed p ps
allowed p [] = False

-- |An application action running with a given list of checked permissions.
newtype AppT (perms :: [Permission]) m a = AppT (ReaderT Env m a)
  deriving (Functor, Applicative, Monad, MonadReader Env, MonadIO)

-- Optional actions run if permissions are available at runtime.
whenRead :: Monad m => AppT (ReadP:perms) m () -> AppT perms m ()
whenRead = unsafeWhen ReadP
whenMeta :: Monad m => AppT (MetaP:perms) m () -> AppT perms m ()
whenMeta = unsafeWhen MetaP
whenWrite :: Monad m => AppT (WriteP:perms) m () -> AppT perms m ()
whenWrite = unsafeWhen WriteP
whenAdmin :: Monad m => AppT (AdminP:perms) m () -> AppT perms m ()
whenAdmin = unsafeWhen AdminP
unsafeWhen :: Monad m => Permission -> AppT perms m () -> AppT perms' m ()
unsafeWhen p act = do
  ps <- asks uperms
  if allowed p ps
    then coerce act
    else return ()

-- |An entry point, requiring a list of permissions
newtype EntryT' (reqP :: [Permission]) (checkedP :: [Permission]) m a
  = EntryT (ReaderT Env m a)
  deriving (Functor, Applicative, Monad, MonadReader Env, MonadIO)
-- |An entry point whose full list of required permission has been (statically) checked).
type EntryT reqP = EntryT' reqP reqP

-- |Run an entry point whose required permissions have been checked.
runEntryT :: MonadIO m => User -> EntryT req m () -> m ()
runEntryT u (EntryT act)
  = case lookup u userDB of
      Nothing   -> say $ "error 401: no such user '" ++ u ++ "'"
      Just perms -> runReaderT act (Env perms u)

-- Functions to build the list of required permissions for an entry point.
requireRead :: MonadIO m => EntryT' r c m () -> EntryT' r (ReadP:c) m ()
requireRead = unsafeRequire ReadP
requireMeta :: MonadIO m => EntryT' r c m () -> EntryT' r (MetaP:c) m ()
requireMeta = unsafeRequire MetaP
requireWrite :: MonadIO m => EntryT' r c m () -> EntryT' r (WriteP:c) m ()
requireWrite = unsafeRequire WriteP
requireAdmin :: MonadIO m => EntryT' r c m () -> EntryT' r (AdminP:c) m ()
requireAdmin = unsafeRequire AdminP
unsafeRequire :: MonadIO m => Permission -> EntryT' r c m () -> EntryT' r c' m ()
unsafeRequire p act = do
  ps <- asks uperms
  if allowed p ps
    then coerce act
    else say $ "error 403: requires permission " ++ show p

-- Adapt an entry point w/ all static checks to an underlying application action.
toRunAppT :: MonadIO m => AppT r m a -> EntryT' r '[] m a
toRunAppT = coerce

-- Example application actions
readPage :: (Allowed ReadP perms ~ True, MonadIO m) => Int -> AppT perms m ()
readPage n = say $ "Read page " ++ show n
metaPage :: (Allowed ReadP perms ~ True, MonadIO m) => Int -> AppT perms m ()
metaPage n = say $ "Secret metadata " ++ show (n^2)
editPage :: (Allowed ReadP perms ~ True, Allowed WriteP perms ~ True, MonadIO m) => Int -> AppT perms m ()
editPage n = say $ "Edit page " ++ show n

say :: MonadIO m => String -> m ()
say = liftIO . putStrLn

-- Example entry points
entryReadPage :: MonadIO m => Int -> EntryT '[ReadP] m ()
entryReadPage n = requireRead . toRunAppT $ do
  readPage n
  whenMeta $ metaPage n
entryEditPage :: MonadIO m => Int -> EntryT '[ReadP, WriteP] m ()
entryEditPage n = requireRead . requireWrite . toRunAppT $ do
  editPage n
  whenMeta $ metaPage n

-- Test harnass
data Req = Read Int
         | Edit Int
         deriving (Read)
main :: IO ()
main = do
  putStr "Username/Req (e.g., \"alice Read 5\"): "
  ln <- getLine
  case break (==' ') ln of
    (user, ' ':rest) -> case read rest of
      Read n -> runEntryT user $ entryReadPage n
      Edit n -> runEntryT user $ entryEditPage n
  main
...