{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ConstraintKinds #-}
module Clod.AdvancedCapability
(
Permission(..)
, ReadPerm
, WritePerm
, ExecutePerm
, AllPerm
, Path
, TypedPath(..)
, PathWithPerm
, Capability(..)
, FileCapability
, DirsCapability
, IsSafePath
, PermissionFor
, HasPermission
, createCapability
, restrictCapability
, withPath
, unsafeAsPath
, readFile
, writeFile
) where
import Prelude hiding (readFile, writeFile)
import Data.Kind (Constraint)
import System.Directory (canonicalizePath)
import qualified Data.ByteString as BS
import Control.Monad.IO.Class (MonadIO, liftIO)
data Permission = Read | Write | Execute | All
type ReadPerm = 'Read
type WritePerm = 'Write
type ExecutePerm = 'Execute
type AllPerm = 'All
data TypedPath (p :: Permission) where
TypedPath :: FilePath -> TypedPath p
instance Show (TypedPath p) where
show :: TypedPath p -> FilePath
show (TypedPath FilePath
path) = FilePath
"TypedPath " FilePath -> ShowS
forall a. [a] -> [a] -> [a]
++ ShowS
forall a. Show a => a -> FilePath
show FilePath
path
instance Eq (TypedPath p) where
(TypedPath FilePath
path1) == :: TypedPath p -> TypedPath p -> Bool
== (TypedPath FilePath
path2) = FilePath
path1 FilePath -> FilePath -> Bool
forall a. Eq a => a -> a -> Bool
== FilePath
path2
type PathWithPerm p = TypedPath p
type Path = String
class HasPermission cap (p :: Permission) | cap -> p
type family IsSafePath (path :: FilePath) (baseDir :: [FilePath]) :: Bool where
IsSafePath path '[] = 'False
IsSafePath path (base ': rest) = OrF (IsSubPath path base) (IsSafePath path rest)
type family OrF (a :: Bool) (b :: Bool) :: Bool where
OrF 'True _ = 'True
OrF 'False b = b
type family IsSubPath (path :: FilePath) (base :: FilePath) :: Bool where
IsSubPath path base = IsPrefix base path
type family IsPrefix (prefix :: FilePath) (str :: FilePath) :: Bool where
IsPrefix prefix str = 'True
type family PermissionFor (required :: Permission) (provided :: Permission) :: Constraint where
PermissionFor 'Read 'Read = ()
PermissionFor 'Read 'All = ()
PermissionFor 'Write 'Write = ()
PermissionFor 'Write 'All = ()
PermissionFor 'Execute 'Execute = ()
PermissionFor 'Execute 'All = ()
PermissionFor 'All 'All = ()
PermissionFor a b = ()
data Capability (p :: Permission) = Capability
{ forall (p :: Permission). Capability p -> [FilePath]
allowedDirs :: [FilePath]
}
type FileCapability = Capability 'Read
type DirsCapability = Capability 'All
createCapability :: forall p. [FilePath] -> Capability p
createCapability :: forall (p :: Permission). [FilePath] -> Capability p
createCapability [FilePath]
dirs = Capability { allowedDirs :: [FilePath]
allowedDirs = [FilePath]
dirs }
restrictCapability :: forall p p'. Capability p -> Capability p'
restrictCapability :: forall (p :: Permission) (p' :: Permission).
Capability p -> Capability p'
restrictCapability Capability p
cap = Capability { allowedDirs :: [FilePath]
allowedDirs = Capability p -> [FilePath]
forall (p :: Permission). Capability p -> [FilePath]
allowedDirs Capability p
cap }
withPath :: forall p m a. (MonadIO m)
=> Capability p -> FilePath -> (Maybe (TypedPath p) -> m a) -> m a
withPath :: forall (p :: Permission) (m :: * -> *) a.
MonadIO m =>
Capability p -> FilePath -> (Maybe (TypedPath p) -> m a) -> m a
withPath Capability p
cap FilePath
path Maybe (TypedPath p) -> m a
f = do
allowed <- IO Bool -> m Bool
forall a. IO a -> m a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO Bool -> m Bool) -> IO Bool -> m Bool
forall a b. (a -> b) -> a -> b
$ [FilePath] -> FilePath -> IO Bool
isPathAllowed (Capability p -> [FilePath]
forall (p :: Permission). Capability p -> [FilePath]
allowedDirs Capability p
cap) FilePath
path
f $ if allowed then Just (TypedPath path) else Nothing
unsafeAsPath :: FilePath -> TypedPath p
unsafeAsPath :: forall (p :: Permission). FilePath -> TypedPath p
unsafeAsPath = FilePath -> TypedPath p
forall (p :: Permission). FilePath -> TypedPath p
TypedPath
readFile :: forall p m. (MonadIO m, PermissionFor 'Read p)
=> Capability p -> TypedPath p -> m BS.ByteString
readFile :: forall (p :: Permission) (m :: * -> *).
(MonadIO m, PermissionFor 'Read p) =>
Capability p -> TypedPath p -> m ByteString
readFile Capability p
_ (TypedPath FilePath
path) = IO ByteString -> m ByteString
forall a. IO a -> m a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO ByteString -> m ByteString) -> IO ByteString -> m ByteString
forall a b. (a -> b) -> a -> b
$ FilePath -> IO ByteString
BS.readFile FilePath
path
writeFile :: forall p m. (MonadIO m, PermissionFor 'Write p)
=> Capability p -> TypedPath p -> BS.ByteString -> m ()
writeFile :: forall (p :: Permission) (m :: * -> *).
(MonadIO m, PermissionFor 'Write p) =>
Capability p -> TypedPath p -> ByteString -> m ()
writeFile Capability p
_ (TypedPath FilePath
path) ByteString
content = IO () -> m ()
forall a. IO a -> m a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> m ()) -> IO () -> m ()
forall a b. (a -> b) -> a -> b
$ FilePath -> ByteString -> IO ()
BS.writeFile FilePath
path ByteString
content
isPathAllowed :: [FilePath] -> FilePath -> IO Bool
isPathAllowed :: [FilePath] -> FilePath -> IO Bool
isPathAllowed [FilePath]
allowedDirs FilePath
path = do
canonicalPath <- FilePath -> IO FilePath
canonicalizePath FilePath
path
checks <- mapM (\FilePath
dir -> do
canonicalDir <- FilePath -> IO FilePath
canonicalizePath FilePath
dir
let isAllowed = FilePath
canonicalDir FilePath -> FilePath -> Bool
forall a. Eq a => a -> a -> Bool
== FilePath
canonicalPath Bool -> Bool -> Bool
||
((FilePath
canonicalDir FilePath -> ShowS
forall a. [a] -> [a] -> [a]
++ FilePath
"/") FilePath -> FilePath -> Bool
forall {a}. Eq a => [a] -> [a] -> Bool
`isPrefixOf` FilePath
canonicalPath)
return isAllowed) allowedDirs
return $ or checks
where
isPrefixOf :: [a] -> [a] -> Bool
isPrefixOf [a]
prefix [a]
str = Int -> [a] -> [a]
forall a. Int -> [a] -> [a]
take ([a] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [a]
prefix) [a]
str [a] -> [a] -> Bool
forall a. Eq a => a -> a -> Bool
== [a]
prefix