packages feed

project-m36-1.2.5: src/lib/ProjectM36/AccessControlList.hs

{-# LANGUAGE DeriveGeneric, DeriveAnyClass, RankNTypes, TupleSections #-}
module ProjectM36.AccessControlList where
import qualified Data.Set as S
import Data.UUID
import Control.DeepSeq (NFData)
import GHC.Generics
import Data.Hashable
import qualified Data.Map as M
import Data.Maybe (fromMaybe)
import Data.Default
import Control.Monad (foldM)

newtype AccessControlList role' permission =
    AccessControlList (M.Map role' (RoleAccess permission))
   deriving (Show, NFData, Generic, Read, Eq)

instance Ord r => Default (AccessControlList r p) where
  def = AccessControlList mempty

instance (Ord p, Eq r, Ord r) => Semigroup (AccessControlList r p) where
  a <> b = merge a b
  
instance (Ord p, Eq r, Ord r) => Monoid (AccessControlList r p) where
  mempty = empty

type MayGrant = Bool

type RoleAccess permission = M.Map permission MayGrant

type RoleId = UUID

-- | Determine if the role can view or alter underlying relation variables.
type RelVarAccessControlList = AccessControlList RoleId RelVarPermission

type FunctionAccessControlList = AccessControlList RoleId FunctionPermission

type AlterTransGraphAccessControlList = AccessControlList RoleId AlterTransGraphPermission

type SchemaAccessControlList = AccessControlList RoleId AlterSchemaPermission

type ACLAccessControlList = AccessControlList RoleId ACLPermission

type DBCFunctionAccessControlList = AccessControlList RoleId DBCFunctionPermission

grantable :: perm -> M.Map perm MayGrant
grantable perm = M.singleton perm True

notGrantable :: perm -> M.Map perm MayGrant
notGrantable perm = M.singleton perm False

class AllPermissions a where
  allPermissions :: S.Set a

data RelVarPermission = AccessRelVarsPermission {-| AccessTypeConstructorsPermission -}
  deriving (Eq, Show, Ord, Generic, NFData, Read, Hashable)

instance AllPermissions RelVarPermission where
  allPermissions = S.fromList [AccessRelVarsPermission]

data FunctionPermission =
  ExecuteFunctionPermission -- ^ allow the role to execute the function
  | ViewFunctionPermission -- ^ allow the role to see that the function exists
  | AlterFunctionPermission -- ^ allow the role to load functions from outside the database or replace existing functions
  deriving (Eq, Show, Ord, Generic, NFData, Hashable)

instance AllPermissions FunctionPermission where
  allPermissions = S.fromList [ExecuteFunctionPermission, ViewFunctionPermission, AlterFunctionPermission]

data AlterTransGraphPermission =
  CommitTransactionPermission -- ^ allow the role to commit a transaction to the graph
  deriving (Eq, Show, Ord, Generic, NFData, Hashable)

instance AllPermissions AlterTransGraphPermission where
  allPermissions = S.fromList [CommitTransactionPermission]

data AlterSchemaPermission =
  AlterSchemaPermission -- ^ allow the role to add or remove a schema
  deriving (Eq, Show, Ord, Generic, NFData, Hashable)

instance AllPermissions AlterSchemaPermission where
  allPermissions = S.fromList [AlterSchemaPermission]

data ACLPermission =
  ViewACLPermission |
  AlterACLPermission
  deriving (Eq, Show, Ord, Generic, NFData, Hashable)

instance AllPermissions ACLPermission where
  allPermissions = S.fromList [ViewACLPermission, AlterACLPermission]

data DBCFunctionPermission =
  ViewDBCFunctionPermission |
  ExecuteDBCFunctionPermission |
  AlterDBCFunctionPermission -- ^ change or delete this function
  deriving (Eq, Show, Ord, Generic, NFData, Hashable)

instance AllPermissions DBCFunctionPermission where
  allPermissions = S.fromList [ViewDBCFunctionPermission,
                               ExecuteDBCFunctionPermission,
                               AlterDBCFunctionPermission]

hasAccess :: (Ord perm, Ord role', Eq role', Eq perm) => [role'] -> perm -> AccessControlList role' perm -> Bool
hasAccess hasRoles checkPerm (AccessControlList acl) =
  any (\role -> M.member checkPerm (fromMaybe mempty (M.lookup role acl))) hasRoles

addAccess :: (Eq role', Ord role', Eq perm, Ord perm) => role' -> perm -> MayGrant -> AccessControlList role' perm -> AccessControlList role' perm
addAccess targetRole newPerm mayGrant (AccessControlList ras) =
  AccessControlList $ M.insertWith M.union targetRole (M.singleton newPerm mayGrant) ras

removeAccess :: (Eq role', Ord role', Eq perm, Ord perm) => role' -> perm -> AccessControlList role' perm -> AccessControlList role' perm
removeAccess targetRole targetPerm (AccessControlList acl) =
  normalize $ AccessControlList $ M.alter alterFunc targetRole acl
  where
    alterFunc (Just ras) = Just $ M.delete targetPerm ras
    alterFunc Nothing = Nothing

-- | Remove empty dictionaries (roles without permissions).
normalize :: AccessControlList r p -> AccessControlList r p
normalize (AccessControlList acl) = AccessControlList $ M.filter (not . M.null) acl

empty :: Ord r => AccessControlList r p
empty = AccessControlList mempty

allPermissionsForRole :: (AllPermissions p, Ord p) => r -> AccessControlList r p
allPermissionsForRole roleid = AccessControlList (M.singleton roleid (M.fromList (map (,True) (S.toList allPermissions))))

permissionForRole :: Ord p => p -> r -> AccessControlList r p
permissionForRole perm roleid = AccessControlList (M.singleton roleid (M.singleton perm False))

merge :: (Ord p, Eq r, Ord r) => AccessControlList r p -> AccessControlList r p -> AccessControlList r p
merge (AccessControlList acl1) (AccessControlList acl2) =
  normalize $ AccessControlList $ M.unionWith mergeFunc acl1 acl2
  where
    mergeFunc = M.unionWith (||)

data DatabaseContextACL =
  DatabaseContextACL {
  relvarsACL :: RelVarAccessControlList,
  dbcFunctionsACL :: FunctionAccessControlList,
  transGraphACL :: AlterTransGraphAccessControlList,
  schemaACL :: SchemaAccessControlList,
  aclACL :: ACLAccessControlList
      }
  deriving (Show, NFData, Generic)

instance Semigroup DatabaseContextACL where
  a <> b = DatabaseContextACL {
    relvarsACL = relvarsACL a <> relvarsACL b,
    dbcFunctionsACL = dbcFunctionsACL a <> dbcFunctionsACL b,
    transGraphACL = transGraphACL a <> transGraphACL b,
    schemaACL = schemaACL a <> schemaACL b,
    aclACL = aclACL a <> aclACL b
    }

instance Monoid DatabaseContextACL where
  mempty = DatabaseContextACL {
    relvarsACL = mempty,
    dbcFunctionsACL = mempty,
    transGraphACL = mempty,
    schemaACL = mempty,
    aclACL = mempty
    }

basic :: DatabaseContextACL
basic = DatabaseContextACL {
  relvarsACL = AccessControlList (M.singleton adminRoleId
                                   (grantableL allPermissions)
                                 ),
  dbcFunctionsACL = AccessControlList (M.singleton adminRoleId
                                        (grantableL allPermissions)
                                      ),
  transGraphACL = AccessControlList (M.singleton adminRoleId
                                      (grantableL allPermissions)
                                    ),
  schemaACL = AccessControlList (M.singleton adminRoleId
                                  (grantableL allPermissions)
                                ),
  aclACL = AccessControlList (M.singleton adminRoleId
                               (grantableL allPermissions)
                             )
  }
  where
    grantableL :: forall perm. Ord perm => S.Set perm -> RoleAccess perm
    grantableL perms = M.fromList (map (, True) (S.toList perms))

adminRoleId :: RoleId
adminRoleId = nil

-- used in access control error reporting
data SomePermission = SomeRelVarPermission RelVarPermission |
                      SomeFunctionPermission FunctionPermission |
                      SomeAlterSchemaPermission AlterSchemaPermission |
                      SomeAlterTransGraphPermission AlterTransGraphPermission |
                      SomeACLPermission ACLPermission |
                      SomeDBCFunctionPermission DBCFunctionPermission
                    deriving (Show, NFData, Generic, Eq, Hashable)

-- | Resolve the first parameter in the AccessContrList, typically a role name resolving to a role id.
resolve :: Ord r2 => (r1 -> IO (Maybe r2)) -> AccessControlList r1 a -> IO (Either r1 (AccessControlList r2 a))
resolve resolver (AccessControlList aclIn) = do
  let folder (Right acc) (r, v) = do
        eR2 <- resolver r
        case eR2 of
          Nothing -> pure (Left r)
          Just r2 -> pure (Right ((r2,v):acc))
      folder (Left acc) _ = pure (Left acc)
  eres <- foldM folder (Right []) (M.toList aclIn)
  case eres of
    Left errR -> pure (Left errR)
    Right res ->
      pure (Right (AccessControlList (M.fromList res)))