В отдельном 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
person
K. A. Buhr
schedule
30.06.2019
[*]
не имеет типов со значениями. Из-за этого средство проверки типов отклонит вашensurePermission
. Вы можете иметь типы вида*
(или#
) только в тех местах, где у вас есть значения, если это имеет смысл. Вам нужно что-то вродеProxy
. - person David Young   schedule 29.06.2019type AppM (perms :: [*]) = ReaderT (Hlist perms) IO
? То есть читатель получает доступ к значению для любого типа в списке типов? - person chi   schedule 29.06.2019Context
и классе типовHasContextEntry
Servant - stackage.org/haddock/lts-12.1/servant-server-0.14.1/ - person Saurabh Nanda   schedule 29.06.2019HList
— это неоднородный список, верно? Какая (хорошо поддерживаемая) библиотека обеспечивает это? Я попытался поискать в stackage/hackage и нашел их много. На самом деле сам Servant предоставляет типHList
, но только для HTTP-заголовков. - person Saurabh Nanda   schedule 29.06.2019perms
по-прежнему имеет тип[*]
(список типов), ноHList :: [*] -> *
преобразует его в простой тип, чтобы его можно было использовать в монаде чтения. Типperms
не имеет значения, имеет значение только тип аргументаReaderT
. - person chi   schedule 29.06.2019PList (perms :: [Permission])
, но компилятору не понравилось то, что я сделал. Я снова редактирую вопрос. - person Saurabh Nanda   schedule 29.06.2019-XDataKinds
, не имеют значений, они представляют собой чисто метаданные уровня типа. Типы типаType
(ранее известные как*
) имеют значения.p
имеет типPermission
, а неType
, и поэтому не может иметь значений. Вы можете попробовать передатьProxy
, параметризованныйp
. У прокси есть параметр фантомного типа, который является многородным. Вы также можете попробовать использовать-XTypeApplications
для указанияp
вместо фактического значения. - person danidiaz   schedule 29.06.2019:t
,:i
и т. д.) и выяснить, какие типы имеют значения, а какие нет? Кроме того, я не понимаю, зачем компилятору беспокоиться о типах, которые могут иметь значения, и типах, которые не могут. Я попробовал следующее -data EmptyType ; foo :: EmptyType -> () ; foo _ = ()
и он скомпилировался. Это происходит из-за':
? Ожидается ли, что оба операнда будут типа*
? Как проверить оператор':
в GHCi? - person Saurabh Nanda   schedule 29.06.2019*
)? Или в том, что оператор уровня типа':
относится к типу* -> [*] -> *
и поэтому не может работать с типами другого типа. - person Saurabh Nanda   schedule 29.06.2019data EmptyType ; foo :: EmptyType -> () ; foo _ = ()
- person Saurabh Nanda   schedule 29.06.2019data PList (perms :: [Permission]) where PCons :: Proxy (p :: Permission) -> PList perms -> PList (p ': perms)
- person Saurabh Nanda   schedule 29.06.2019Proxy
компилируется, потому что типProxy
имеет конструктор значений, также названныйProxy
. Однако значение не используется,Proxy
служит только обходным путем для передачи информации на уровне типа. Обратите внимание, что тип конструктора типаProxy
похож наforall {k}. k -> Type
. Вы даете ему параметр (фантомного) типа любого типа, и он дает вам тип видаType
(также известный как*
), который может иметь значения, а именно значениеProxy
. - person danidiaz   schedule 29.06.2019[*]
, не имеет типов, которые содержат значения. 2) Компилятор не позволит типам вида[*]
(и подобным) встречаться в позициях значений (на самом деле не уверен, какая точная фраза будет здесь, поэтому дайте мне знать, если это не очень ясно). 2 не полностью из-за 1, но они оба (по отдельности) верны. - person David Young   schedule 29.06.2019[*]
даже не будут компилироваться в такой позиции, они на самом деле не имеют никаких значений. Но на самом деле это не совсем то, что проверяет компилятор, когда он отклоняет тип, подобныйf :: ['a','b','c'] -> ()
. - person David Young   schedule 29.06.2019vinyl
в хорошем состоянии, аHList
в основномRec Identity
. - person dfeuer   schedule 29.06.2019