336 lines
14 KiB
Haskell
336 lines
14 KiB
Haskell
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
module ChuSQL.Server.Privileges (
|
|
Grant (..),
|
|
Membership (..),
|
|
PrivilegeCommand (..),
|
|
PrivilegeError (..),
|
|
Privileges,
|
|
RoleView (..),
|
|
authorize,
|
|
authorizeTables,
|
|
filterTables,
|
|
listRoleViews,
|
|
newPrivileges,
|
|
normalizeObject,
|
|
privilegeCommand,
|
|
runPrivilegeCommand,
|
|
) where
|
|
|
|
import ChuSQL.Core.Engine.Syntax.AST
|
|
import ChuSQL.Server.Accounts (Principal, principalIsRoot, principalName)
|
|
import ChuSQL.Interface.Protocol (Grant (..), RoleView (..))
|
|
import ChuSQL.Server.Backend (
|
|
Backend (..),
|
|
exprTables,
|
|
systemDatabaseName,
|
|
tableRefsOf,
|
|
)
|
|
import ChuSQL.Server.Catalog (
|
|
Catalog,
|
|
addGrant,
|
|
addMember,
|
|
dropRole,
|
|
eachAction,
|
|
insertRole,
|
|
newCatalog,
|
|
normalizeObject,
|
|
normalizeRole,
|
|
normalizeUser,
|
|
readGrants,
|
|
readMembers,
|
|
readRoleNames,
|
|
removeGrant,
|
|
removeMember,
|
|
withCatalogLock,
|
|
ensureCatalog,
|
|
)
|
|
import Data.List (nub, sort)
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
|
|
-- 角色与权限服务:角色、授权与成员关系落在 system 库的表里,并据此判权限。
|
|
|
|
-- | 出错口径与账号服务一致
|
|
data PrivilegeError = PrivilegeError Text Text
|
|
deriving (Show, Eq)
|
|
|
|
-- 一条角色授权(定义在共享协议层)
|
|
-- | 角色成员资格
|
|
data Membership = Membership
|
|
{ memberRole :: Text
|
|
, memberUser :: Text
|
|
}
|
|
deriving (Show, Eq, Ord)
|
|
|
|
-- | 一个角色的全貌:授权与成员
|
|
|
|
data PrivilegeCommand
|
|
= CreateRoleCommand Text
|
|
| DropRoleCommand Text
|
|
| GrantPrivilegesCommand [Text] Text Text
|
|
| RevokePrivilegesCommand [Text] Text Text
|
|
| GrantRoleCommand Text [Text]
|
|
| RevokeRoleCommand Text [Text]
|
|
deriving (Show, Eq)
|
|
|
|
data Privileges = Privileges
|
|
{ pvCatalog :: Catalog
|
|
}
|
|
|
|
-- | 建权限服务,固定在 system 库上
|
|
newPrivileges :: Backend -> IO Privileges
|
|
newPrivileges base = Privileges <$> newCatalog (beWithDatabase base systemDatabaseName)
|
|
|
|
-- | 语句转管理命令,不认的给 Nothing
|
|
privilegeCommand :: Statement -> Maybe PrivilegeCommand
|
|
privilegeCommand stmt = case stmt of
|
|
CreateRole name -> Just (CreateRoleCommand (T.pack name))
|
|
DropRole name -> Just (DropRoleCommand (T.pack name))
|
|
GrantPrivileges privs obj role ->
|
|
Just (GrantPrivilegesCommand (map T.toLower (map T.pack privs)) (T.pack obj) (T.pack role))
|
|
RevokePrivileges privs obj role ->
|
|
Just (RevokePrivilegesCommand (map T.toLower (map T.pack privs)) (T.pack obj) (T.pack role))
|
|
GrantRole role members -> Just (GrantRoleCommand (T.pack role) (map T.pack members))
|
|
RevokeRole role members -> Just (RevokeRoleCommand (T.pack role) (map T.pack members))
|
|
_ -> Nothing
|
|
|
|
-- | 执行管理命令:只有管理员能跑,命令提到的角色必须已经存在
|
|
runPrivilegeCommand :: Privileges -> Principal -> Text -> PrivilegeCommand -> IO (Either PrivilegeError ())
|
|
runPrivilegeCommand service principal database command
|
|
| not (principalIsRoot principal) = pure (Left forbidden)
|
|
| needsDatabase = pure (Left (PrivilegeError "no_database" "no database selected"))
|
|
| otherwise = withTables service (applyCommand service database command)
|
|
where
|
|
-- | 授权到具体对象时需要先选库
|
|
needsDatabase = T.null database && case command of
|
|
GrantPrivilegesCommand _ object _ -> T.strip object /= "*"
|
|
RevokePrivilegesCommand _ object _ -> T.strip object /= "*"
|
|
_ -> False
|
|
|
|
-- | 语句鉴权:管理员全通,普通身份按需查
|
|
authorize :: Privileges -> Principal -> Text -> Statement -> IO (Either PrivilegeError ())
|
|
authorize service principal database stmt
|
|
| principalIsRoot principal = pure (Right ())
|
|
| otherwise = case requiredActions stmt of
|
|
Nothing -> pure (Left forbidden)
|
|
Just needed -> authorizeTables service principal database needed
|
|
|
|
-- | 直接查一组 (表, 权限):一键接口用这个
|
|
authorizeTables :: Privileges -> Principal -> Text -> [(Text, Text)] -> IO (Either PrivilegeError ())
|
|
authorizeTables service principal database needed
|
|
| principalIsRoot principal = pure (Right ())
|
|
| otherwise = withTables service $ do
|
|
grants <- grantsOfUser service (principalName principal)
|
|
pure (grants >>= \found -> checkAll database found needed)
|
|
|
|
-- | 只留下用户有 SELECT 权的表
|
|
filterTables :: Privileges -> Principal -> Text -> [Text] -> IO (Either PrivilegeError [Text])
|
|
filterTables service principal database tables
|
|
| principalIsRoot principal = pure (Right tables)
|
|
| otherwise = withTables service $ do
|
|
grants <- grantsOfUser service (principalName principal)
|
|
pure ((\found -> [table | table <- tables, checkAll database found [(table, "select")] == Right ()]) <$> grants)
|
|
|
|
-- | 角色总览(REST 与测试用)
|
|
listRoleViews :: Privileges -> IO (Either PrivilegeError [RoleView])
|
|
listRoleViews service = withTables service $ do
|
|
names <- roleNames service
|
|
case names of
|
|
Left err -> pure (Left err)
|
|
Right known -> sequence <$> mapM view (sort known)
|
|
where
|
|
-- | 组装一个角色的总览
|
|
view role = do
|
|
grants <- roleGrantsOf service role
|
|
members <- membersOfRole service role
|
|
pure (RoleView role <$> grants <*> members)
|
|
|
|
-- | 一条语句需要哪些 (表, 权限)
|
|
requiredActions :: Statement -> Maybe [(Text, Text)]
|
|
requiredActions stmt = case stmt of
|
|
Select{} -> Just (selectNeeds (selectFrom stmt) (maybe [] (: []) (selectWhere stmt)))
|
|
SelectExpr{} ->
|
|
Just (selectNeeds (selectFrom stmt) (maybe [] (: []) (selectWhere stmt) ++ map snd (selectItems stmt)))
|
|
Insert table _ rows -> Just ((T.pack table, "insert") : selectsOf (concat rows))
|
|
Update table assigns cond -> Just ((T.pack table, "update") : selectsOf (map snd assigns ++ maybe [] (: []) cond))
|
|
Delete table cond -> Just ((T.pack table, "delete") : selectsOf (maybe [] (: []) cond))
|
|
_ -> Nothing
|
|
where
|
|
-- | FROM 子句要的 SELECT 权限
|
|
selectNeeds source exprs = [(ref, "select") | ref <- nub (tableRefsOf source)] ++ selectsOf exprs
|
|
-- | 表达式里子查询要的 SELECT 权限
|
|
selectsOf exprs = [(ref, "select") | ref <- nub (concatMap exprTables exprs)]
|
|
|
|
-- | 每项 (表, 权限) 都要被某条授权覆盖
|
|
checkAll :: Text -> [Grant] -> [(Text, Text)] -> Either PrivilegeError ()
|
|
checkAll database grants needed = case [pair | pair@(table, privilege) <- needed, not (covered table privilege)] of
|
|
[] -> Right ()
|
|
((table, privilege) : _) ->
|
|
Left (PrivilegeError "forbidden" ("permission denied: " <> T.toUpper privilege <> " ON " <> table))
|
|
where
|
|
-- | 某一项是否被授权覆盖
|
|
covered table privilege =
|
|
let target = normalizeObject database table
|
|
in any
|
|
(\grant -> (grantObject grant == "*" || grantObject grant == target) && grantPrivilege grant == privilege)
|
|
grants
|
|
|
|
-- | 一个用户实际能用的授权:所属角色加上继承来的
|
|
grantsOfUser :: Privileges -> Text -> IO (Either PrivilegeError [Grant])
|
|
grantsOfUser service user = do
|
|
members <- catalog (readMembers (pvCatalog service))
|
|
grants <- catalog (readGrants (pvCatalog service))
|
|
roles <- roleNames service
|
|
pure $ do
|
|
memberRows <- members
|
|
grantRows <- grants
|
|
known <- roles
|
|
let edges = roleEdges memberRows known
|
|
mine = closureUp edges [role | (role, member) <- memberRows, member == normalizeUser user]
|
|
pure [grant | grant <- grantRows, grantRole grant `elem` mine]
|
|
|
|
-- | 角色到角色的成员边:父角色在前
|
|
roleEdges :: [(Text, Text)] -> [Text] -> [(Text, Text)]
|
|
roleEdges members roles = [pair | pair@(_, child) <- members, child `elem` roles]
|
|
|
|
-- | 向上闭包:这群角色直接或间接所属的全部角色
|
|
closureUp :: [(Text, Text)] -> [Text] -> [Text]
|
|
closureUp edges = go
|
|
where
|
|
-- | 展开一轮,没有再新增就停
|
|
go known =
|
|
let more = [parent | (parent, child) <- edges, child `elem` known, parent `notElem` known]
|
|
in if null more then known else go (known ++ more)
|
|
|
|
-- | 一个角色挂着的授权
|
|
roleGrantsOf :: Privileges -> Text -> IO (Either PrivilegeError [Grant])
|
|
roleGrantsOf service role = do
|
|
grants <- catalog (readGrants (pvCatalog service))
|
|
pure (filter (\grant -> grantRole grant == normalizeRole role) <$> grants)
|
|
|
|
-- | 角色名清单(统一小写)
|
|
roleNames :: Privileges -> IO (Either PrivilegeError [Text])
|
|
roleNames service = catalog (readRoleNames (pvCatalog service))
|
|
|
|
-- | 一个角色的直接成员:用户与子角色
|
|
membersOfRole :: Privileges -> Text -> IO (Either PrivilegeError [Text])
|
|
membersOfRole service role = do
|
|
members <- catalog (readMembers (pvCatalog service))
|
|
pure (map snd . filter (\(name, _) -> name == normalizeRole role) <$> members)
|
|
|
|
-- | 角色必须存在,否则命令不落地
|
|
requireRole :: Privileges -> Text -> IO (Either PrivilegeError ())
|
|
requireRole service role = do
|
|
known <- roleNames service
|
|
pure $ case known of
|
|
Left err -> Left err
|
|
Right names
|
|
| normalizeRole role `elem` names -> Right ()
|
|
| otherwise -> Left (unknownRole role)
|
|
|
|
-- | 真正落库(表已经保证存在)
|
|
applyCommand :: Privileges -> Text -> PrivilegeCommand -> IO (Either PrivilegeError ())
|
|
applyCommand service database command = case command of
|
|
CreateRoleCommand role -> do
|
|
known <- roleNames service
|
|
case known of
|
|
Left err -> pure (Left err)
|
|
Right names
|
|
| normalizeRole role `elem` names -> pure (Left (PrivilegeError "conflict" ("role already exists: " <> role)))
|
|
| otherwise -> catalog (insertRole (pvCatalog service) role)
|
|
DropRoleCommand role -> do
|
|
allowed <- requireRole service role
|
|
case allowed of
|
|
Left err -> pure (Left err)
|
|
Right () -> catalog (dropRole (pvCatalog service) role)
|
|
GrantPrivilegesCommand privileges object role -> do
|
|
allowed <- requireRole service role
|
|
case allowed of
|
|
Left err -> pure (Left err)
|
|
Right () ->
|
|
eachAction (expandPrivileges privileges) $ \privilege ->
|
|
catalog (addGrant (pvCatalog service) role privilege (normalizeObject database object))
|
|
RevokePrivilegesCommand privileges object role -> do
|
|
allowed <- requireRole service role
|
|
case allowed of
|
|
Left err -> pure (Left err)
|
|
Right () ->
|
|
eachAction (expandPrivileges privileges) $ \privilege ->
|
|
catalog (removeGrant (pvCatalog service) role privilege (normalizeObject database object))
|
|
GrantRoleCommand role members -> do
|
|
allowed <- requireRole service role
|
|
case allowed of
|
|
Left err -> pure (Left err)
|
|
Right () -> do
|
|
known <- roleNames service
|
|
case known of
|
|
Left err -> pure (Left err)
|
|
Right roles -> eachAction members (grantMember service roles role)
|
|
RevokeRoleCommand role users -> do
|
|
allowed <- requireRole service role
|
|
case allowed of
|
|
Left err -> pure (Left err)
|
|
Right () -> eachAction users $ \user -> catalog (removeMember (pvCatalog service) role user)
|
|
|
|
-- | 加一条成员边:名字是角色就查环,否则当用户加
|
|
grantMember :: Privileges -> [Text] -> Text -> Text -> IO (Either PrivilegeError ())
|
|
grantMember service roles role member = do
|
|
current <- membersOfRole service role
|
|
case current of
|
|
Left err -> pure (Left err)
|
|
Right known
|
|
| normalizeUser member `elem` known -> pure (Right ())
|
|
| normalizeRole member `notElem` roles -> catalog (addMember (pvCatalog service) role member)
|
|
| otherwise -> do
|
|
edges <- membershipEdges service
|
|
case edges of
|
|
Left err -> pure (Left err)
|
|
Right pairs
|
|
| normalizeRole member `elem` closureUp pairs [normalizeRole role] ->
|
|
pure (Left (cycleMember role member))
|
|
| otherwise -> catalog (addMember (pvCatalog service) role member)
|
|
|
|
-- | 只保留两端都是角色的成员边
|
|
membershipEdges :: Privileges -> IO (Either PrivilegeError [(Text, Text)])
|
|
membershipEdges service = do
|
|
members <- catalog (readMembers (pvCatalog service))
|
|
roles <- roleNames service
|
|
pure (roleEdges <$> members <*> roles)
|
|
|
|
-- | 全程持锁,先保证系统表存在再跑动作
|
|
withTables :: Privileges -> IO (Either PrivilegeError a) -> IO (Either PrivilegeError a)
|
|
withTables service action = withCatalogLock (pvCatalog service) $ do
|
|
ready <- ensureCatalog (pvCatalog service)
|
|
case ready of
|
|
Left err -> pure (Left (storageError err))
|
|
Right () -> action
|
|
|
|
-- | 目录操作转成权限错误
|
|
catalog :: IO (Either String a) -> IO (Either PrivilegeError a)
|
|
catalog action = fmap (either (Left . storageError) Right) action
|
|
|
|
-- | 把 ALL 展开成四种具体权限
|
|
expandPrivileges :: [Text] -> [Text]
|
|
expandPrivileges privileges = nub (concatMap expand (map (T.toLower . T.strip) privileges))
|
|
where
|
|
-- | 展开一条权限名
|
|
expand "all" = ["select", "insert", "update", "delete"]
|
|
expand other = [other]
|
|
|
|
forbidden :: PrivilegeError
|
|
forbidden = PrivilegeError "forbidden" "administrator required"
|
|
|
|
-- | 未知角色的错误
|
|
unknownRole :: Text -> PrivilegeError
|
|
unknownRole role = PrivilegeError "not_found" ("unknown role: " <> role)
|
|
|
|
-- | 角色成员成环的错误
|
|
cycleMember :: Text -> Text -> PrivilegeError
|
|
cycleMember role member =
|
|
PrivilegeError "conflict" ("role membership would create a cycle: " <> normalizeRole role <> " and " <> normalizeRole member)
|
|
|
|
-- | 存储错误转成权限错误
|
|
storageError :: String -> PrivilegeError
|
|
storageError message = PrivilegeError "storage_error" ("privilege storage: " <> T.pack message)
|