修bug,完善功能
This commit is contained in:
1 parent
30364b1c4b
commit
6996e19694
125 files changed
+15343
-3746
No files matched your search
+20
-16
@@ -2,7 +2,6 @@
|
||||
|
||||
module Main (main) where
|
||||
|
||||
import Control.Monad (when)
|
||||
import ChuSQL.Core.Engine.Storage.FFI (storageVersion)
|
||||
import ChuSQL.Core.Engine.Storage.IPC (closeConnection, localStorageLink, setStorageLink)
|
||||
import ChuSQL.Interface.Config (
|
||||
@@ -23,7 +22,8 @@ import ChuSQL.Server.TCP (
|
||||
newServerEnv,
|
||||
runServer,
|
||||
)
|
||||
import Control.Exception (IOException, try, bracket_)
|
||||
import Control.Exception (IOException, bracket_, try)
|
||||
import Control.Monad (when)
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import Data.Text (Text)
|
||||
@@ -64,7 +64,7 @@ emptyOptions =
|
||||
parseArgs :: [String] -> Either String Options
|
||||
parseArgs = go emptyOptions
|
||||
where
|
||||
-- | 逐条消费参数列表
|
||||
-- \| 逐条消费参数列表
|
||||
go opts [] = Right opts
|
||||
go opts ("--help" : rest) = go opts{optHelp = True} rest
|
||||
go opts ("-h" : rest) = go opts{optHelp = True} rest
|
||||
@@ -76,7 +76,7 @@ parseArgs = go emptyOptions
|
||||
go opts ("--max-rows" : v : rest) = withInt "--max-rows" v (\n -> go opts{optMaxRows = Just n} rest)
|
||||
go _ (a : _) = Left ("unrecognized argument or missing value: " ++ a)
|
||||
|
||||
-- | 把参数值读成整数,读不出就报错
|
||||
-- \| 把参数值读成整数,读不出就报错
|
||||
withInt :: String -> String -> (Int -> Either String Options) -> Either String Options
|
||||
withInt name v k = maybe (Left (name ++ " needs an integer, got: " ++ v)) k (readMaybe v)
|
||||
|
||||
@@ -179,7 +179,9 @@ run opts = do
|
||||
env <- newServerEnv backend rootName configPath config limiter
|
||||
let announce port = putStrLn ("listening: tcp://" ++ scHost config ++ ":" ++ show port)
|
||||
bracket_
|
||||
(pure link) closeConnection (serve env announce)
|
||||
(pure link)
|
||||
closeConnection
|
||||
(serve env announce)
|
||||
|
||||
-- | 起 TCP 服务并处理绑定失败
|
||||
serve :: ServerEnv -> (Int -> IO ()) -> IO ()
|
||||
@@ -205,7 +207,7 @@ checkInitialized backend = do
|
||||
putStrLn "!! For a damaged existing system catalog, stop all processes and try csql-bootstrap repair or recover."
|
||||
exitFailure
|
||||
where
|
||||
-- | 应答里的 initialized 是不是 true
|
||||
-- \| 应答里的 initialized 是不是 true
|
||||
statusInitialized (A.Object fields) = KM.lookup "initialized" fields == Just (A.Bool True)
|
||||
statusInitialized _ = False
|
||||
|
||||
@@ -213,17 +215,19 @@ checkInitialized backend = do
|
||||
reportCredential :: Backend -> Text -> Bool -> IO ()
|
||||
reportCredential backend rootName legacy = do
|
||||
putStrLn ("administrator: " ++ T.unpack rootName)
|
||||
when legacy
|
||||
$ do putStrLn
|
||||
" !! the [web] section still carries a plaintext password key; it is ignored now"
|
||||
putStrLn
|
||||
" !! remove it: the administrator password lives in the __system_users system table"
|
||||
when legacy $
|
||||
do
|
||||
putStrLn
|
||||
" !! the [web] section still carries a plaintext password key; it is ignored now"
|
||||
putStrLn
|
||||
" !! remove it: the administrator password lives in the __system_users system table"
|
||||
passwordless <- administratorPasswordless backend rootName
|
||||
when passwordless
|
||||
$ do putStrLn
|
||||
" !! the administrator has no password yet: sign in with an empty password, then set one"
|
||||
putStrLn
|
||||
" !! ALTER USER root IDENTIFIED BY '...' (or use the account page in the Web UI)"
|
||||
when passwordless $
|
||||
do
|
||||
putStrLn
|
||||
" !! the administrator has no password yet: sign in with an empty password, then set one"
|
||||
putStrLn
|
||||
" !! ALTER USER root IDENTIFIED BY '...' (or use the account page in the Web UI)"
|
||||
|
||||
-- | 探测存储是否可用,答不上 ping 就退出
|
||||
checkStorage :: Backend -> IO ()
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module ChuSQL.Server.Accounts (
|
||||
Accounts, AccountError (..), Principal (..), PasswordPolicy (..), defaultPasswordPolicy,
|
||||
newAccounts, accountsPolicy, setAccountsPolicy, passwordAllowed,
|
||||
ensureRootAccount, administratorPasswordless, rootUserName, principalName, principalIsRoot, principalIsCatalogManager,
|
||||
authenticate, authenticateSudo, currentPrincipal,
|
||||
listAccounts, findAccount, AccountCommand (..), accountCommand, runAccountCommand,
|
||||
Accounts,
|
||||
AccountError (..),
|
||||
Principal (..),
|
||||
PasswordPolicy (..),
|
||||
defaultPasswordPolicy,
|
||||
newAccounts,
|
||||
accountsPolicy,
|
||||
setAccountsPolicy,
|
||||
passwordAllowed,
|
||||
ensureRootAccount,
|
||||
administratorPasswordless,
|
||||
rootUserName,
|
||||
principalName,
|
||||
principalIsRoot,
|
||||
principalIsCatalogManager,
|
||||
authenticate,
|
||||
authenticateSudo,
|
||||
currentPrincipal,
|
||||
listAccounts,
|
||||
findAccount,
|
||||
AccountCommand (..),
|
||||
accountCommand,
|
||||
runAccountCommand,
|
||||
) where
|
||||
|
||||
import ChuSQL.Core.Engine.Error (accountErrorCode)
|
||||
@@ -32,7 +50,7 @@ data Principal
|
||||
deriving (Eq)
|
||||
|
||||
instance Show Principal where
|
||||
-- | 身份的可读形式
|
||||
-- \| 身份的可读形式
|
||||
show (Root name) = "root " ++ show name
|
||||
show (Ordinary a) = "account " ++ show (accountUser a)
|
||||
|
||||
@@ -48,9 +66,10 @@ principalIsRoot (Ordinary a) = accountIsSuperuser a && accountEnabled a && accou
|
||||
|
||||
-- | 判断身份是否可管理系统目录
|
||||
principalIsCatalogManager :: Principal -> Bool
|
||||
principalIsCatalogManager principal = principalIsRoot principal || case principal of
|
||||
Ordinary a -> accountSystemCatalogManager a && accountEnabled a && accountCanLogin a
|
||||
Root _ -> True
|
||||
principalIsCatalogManager principal =
|
||||
principalIsRoot principal || case principal of
|
||||
Ordinary a -> accountSystemCatalogManager a && accountEnabled a && accountCanLogin a
|
||||
Root _ -> True
|
||||
|
||||
data Accounts = Accounts
|
||||
{ acBackend :: Backend
|
||||
@@ -94,12 +113,13 @@ passwordAllowed policy password
|
||||
| classes < ppClasses policy = bad "password needs more character classes"
|
||||
| otherwise = Right ()
|
||||
where
|
||||
-- | 构造一条请求错误
|
||||
-- \| 构造一条请求错误
|
||||
bad = Left . AccountError "bad_request"
|
||||
-- | 命中的字符类别数
|
||||
-- \| 命中的字符类别数
|
||||
classes =
|
||||
length
|
||||
( filter id
|
||||
( filter
|
||||
id
|
||||
[ T.any isAsciiLower password
|
||||
, T.any isAsciiUpper password
|
||||
, T.any isDigit password
|
||||
@@ -114,7 +134,7 @@ validateUserName name
|
||||
Left (AccountError "bad_request" "invalid account name")
|
||||
| otherwise = Right ()
|
||||
where
|
||||
-- | 该字符是否允许使用
|
||||
-- \| 该字符是否允许使用
|
||||
allowed c = isAscii c && (isAlphaNum c || c `elem` ("_.-" :: String))
|
||||
|
||||
-- | 校验新身份的名称
|
||||
@@ -132,7 +152,7 @@ storageResult :: Either String [Account] -> Either AccountError ()
|
||||
storageResult (Right _) = Right ()
|
||||
storageResult (Left message) = Left (AccountError code text)
|
||||
where
|
||||
-- | 认得的码原样用,其余存储细节不外泄
|
||||
-- \| 认得的码原样用,其余存储细节不外泄
|
||||
code = T.pack (accountErrorCode message)
|
||||
text = if code == failureCode then failureText else T.pack message
|
||||
AccountError failureCode failureText = storageFailure
|
||||
@@ -200,18 +220,18 @@ authenticate service user password = withMVar (acLock service) $ \_ -> do
|
||||
where
|
||||
name = normalize user
|
||||
ordinaryRefused = AccountError "forbidden" "this server only accepts the administrator"
|
||||
-- | 判断是否有已设置口令的登录管理员
|
||||
-- \| 判断是否有已设置口令的登录管理员
|
||||
unset accounts = not (any (\a -> accountIsSuperuser a && accountEnabled a && accountCanLogin a && not (T.null (accountHash a))) accounts)
|
||||
-- | 管理员登录
|
||||
-- \| 管理员登录
|
||||
signInRoot accounts = case filter ((== name) . accountUser) accounts of
|
||||
[a] | T.null (accountHash a) && T.null password -> issue a
|
||||
[a] | accountCanLogin a && accountEnabled a && not (T.null (accountHash a)) && verifyPassword (accountHash a) password -> issue a
|
||||
_ -> pure (Left unauthorized)
|
||||
-- | 普通账号登录
|
||||
-- \| 普通账号登录
|
||||
signInOrdinary accounts = case filter ((== name) . accountUser) accounts of
|
||||
[a] | accountCanLogin a && accountEnabled a && not (T.null (accountHash a)) && verifyPassword (accountHash a) password -> issue a
|
||||
_ -> pure (Left unauthorized)
|
||||
-- | 记一次登录再发会话
|
||||
-- \| 记一次登录再发会话
|
||||
issue a = do
|
||||
stamp <- currentStamp
|
||||
written <- beAccounts (acBackend service) (ReqAccountLogin (accountUser a) (Just stamp))
|
||||
@@ -279,8 +299,8 @@ resetPassword service name password = do
|
||||
dropUser :: Accounts -> Text -> IO (Either AccountError ())
|
||||
dropUser service name =
|
||||
case validateUserName name of
|
||||
Left err -> pure (Left err)
|
||||
Right () -> storageResult <$> beAccounts (acBackend service) (ReqAccountDrop (normalize name))
|
||||
Left err -> pure (Left err)
|
||||
Right () -> storageResult <$> beAccounts (acBackend service) (ReqAccountDrop (normalize name))
|
||||
|
||||
-- | 账号管理命令:SQL 语句与一键接口在这上面汇合
|
||||
data AccountCommand
|
||||
@@ -302,7 +322,7 @@ accountCommand _ = Nothing
|
||||
runAccountCommand :: Accounts -> Principal -> AccountCommand -> IO (Either AccountError ())
|
||||
runAccountCommand service principal command = withMVar (acLock service) $ const run
|
||||
where
|
||||
-- | 校验并执行身份管理权限
|
||||
-- \| 校验并执行身份管理权限
|
||||
run
|
||||
| not (principalIsCatalogManager principal) = pure (Left (AccountError "forbidden" "catalog manager required"))
|
||||
| not (principalIsRoot principal) = do
|
||||
@@ -311,20 +331,21 @@ runAccountCommand service principal command = withMVar (acLock service) $ const
|
||||
Left err -> pure (Left err)
|
||||
Right accounts
|
||||
| any (\a -> accountUser a == normalize (target command) && (accountIsSuperuser a || accountSystemCatalogManager a || accountAllowSudoAuth a)) accounts
|
||||
|| privilegedChange command -> pure (Left (AccountError "forbidden" "superuser required to manage privileged identities"))
|
||||
|| privilegedChange command ->
|
||||
pure (Left (AccountError "forbidden" "superuser required to manage privileged identities"))
|
||||
| otherwise -> execute
|
||||
| otherwise = execute
|
||||
-- | 获取身份管理的目标
|
||||
-- \| 获取身份管理的目标
|
||||
target (CreateAccount name _) = name
|
||||
target (ResetAccountPassword name _) = name
|
||||
target (DropAccount name) = name
|
||||
target (AlterAccountAttributes name _) = name
|
||||
-- | 判断是否修改特权属性
|
||||
-- \| 判断是否修改特权属性
|
||||
privilegedChange (AlterAccountAttributes _ attributes) = any (\(key, _) -> key `elem` ["superuser", "system_catalog_manager", "allow_sudo_auth"]) attributes
|
||||
privilegedChange _ = False
|
||||
-- | 执行已授权的身份命令
|
||||
-- \| 执行已授权的身份命令
|
||||
execute = executeWith (if principalIsRoot principal then service else service{acBackend = (acBackend service){beAccounts = beAccounts (acBackend service) . ReqCatalogManage}})
|
||||
-- | 通过受限存储请求执行管理操作
|
||||
-- \| 通过受限存储请求执行管理操作
|
||||
executeWith managed = case command of
|
||||
CreateAccount name _ | normalize name == "public" -> pure (Left (AccountError "bad_request" "public is reserved for default privileges"))
|
||||
CreateAccount name password -> createUser managed name password
|
||||
@@ -333,5 +354,14 @@ runAccountCommand service principal command = withMVar (acLock service) $ const
|
||||
AlterAccountAttributes name attributes
|
||||
| length (nub (map fst attributes)) /= length attributes -> pure (Left (AccountError "bad_request" "duplicate identity attribute"))
|
||||
| otherwise ->
|
||||
storageResult <$> beAccounts (acBackend managed) (ReqIdentityAlter (normalize name)
|
||||
(lookup "login" attributes) (lookup "superuser" attributes) (lookup "enabled" attributes) (lookup "system_catalog_manager" attributes) (lookup "allow_sudo_auth" attributes))
|
||||
storageResult
|
||||
<$> beAccounts
|
||||
(acBackend managed)
|
||||
( ReqIdentityAlter
|
||||
(normalize name)
|
||||
(lookup "login" attributes)
|
||||
(lookup "superuser" attributes)
|
||||
(lookup "enabled" attributes)
|
||||
(lookup "system_catalog_manager" attributes)
|
||||
(lookup "allow_sudo_auth" attributes)
|
||||
)
|
||||
@@ -1,8 +1,10 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module ChuSQL.Server.Backend (
|
||||
StatementResult (..),
|
||||
Backend (..),
|
||||
applyTransactionMemory,
|
||||
columnsFromStatement,
|
||||
currentStamp,
|
||||
exprTables,
|
||||
@@ -21,11 +23,9 @@ module ChuSQL.Server.Backend (
|
||||
tableRefsOf,
|
||||
) where
|
||||
|
||||
import Data.Maybe (fromMaybe, mapMaybe, maybeToList)
|
||||
import ChuSQL.Core.Engine (runStatement, runStatementM)
|
||||
import ChuSQL.Core.Model
|
||||
import ChuSQL.Core.Engine.Semantic (prepare)
|
||||
import ChuSQL.Core.Engine.Storage (MonadStorage (schema, snapshot))
|
||||
import ChuSQL.Core.Engine.Storage (MonadStorage (schema))
|
||||
import ChuSQL.Core.Engine.Storage.IPC (
|
||||
Account (..),
|
||||
Env,
|
||||
@@ -34,22 +34,27 @@ import ChuSQL.Core.Engine.Storage.IPC (
|
||||
Response (..),
|
||||
SchemaColumn (..),
|
||||
TableInfo (..),
|
||||
databaseSnapshot,
|
||||
envForDatabase,
|
||||
sendRawRequest,
|
||||
sendRequest,
|
||||
snapshotDatabase,
|
||||
)
|
||||
import ChuSQL.Core.Engine.Syntax.AST
|
||||
import ChuSQL.Core.Engine.Syntax.Parser (parseStatement)
|
||||
import ChuSQL.Core.Model
|
||||
import ChuSQL.Core.Protocol (TxnOp (..))
|
||||
import Control.Concurrent.MVar (MVar, modifyMVar)
|
||||
import Data.List (nub, isPrefixOf)
|
||||
import Data.Text (Text)
|
||||
import Data.Time (getCurrentTime)
|
||||
import Data.Time.Format (defaultTimeLocale, formatTime)
|
||||
import Control.Concurrent.MVar (MVar, modifyMVar, newMVar)
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||
import Data.List (isPrefixOf, nub)
|
||||
import Data.Maybe (fromMaybe, mapMaybe, maybeToList)
|
||||
import qualified Data.Set as Set
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time (getCurrentTime)
|
||||
import Data.Time.Format (defaultTimeLocale, formatTime)
|
||||
|
||||
-- 后端抽象:跑语句、取数据字典与账号,分 IPC(真存储)和内存(测试)两套实现。
|
||||
|
||||
@@ -59,18 +64,24 @@ data StatementResult = StatementResult
|
||||
}
|
||||
|
||||
data Backend = Backend
|
||||
{ beStatement :: String -> IO (Either String StatementResult)
|
||||
{ beConcurrentTransactions :: Bool
|
||||
, beBeginTransaction :: Bool -> IO (Either String (Maybe String, Database))
|
||||
, beStageTransaction :: String -> [TxnOp] -> [String] -> IO (Either String ())
|
||||
, bePreviewTransaction :: String -> IO (Either String (String, Database))
|
||||
, beCommitTransaction :: String -> String -> IO (Either String ())
|
||||
, beRollbackTransaction :: String -> IO (Either String ())
|
||||
, beStatement :: String -> IO (Either String StatementResult)
|
||||
, beCatalog :: IO (Either String [TableInfo])
|
||||
, bePing :: IO Bool
|
||||
, beAccounts :: Request -> IO (Either String [Account])
|
||||
, beWithDatabase :: String -> Backend
|
||||
, beDatabases :: IO (Either String [String])
|
||||
-- | 原样转发一条存储请求
|
||||
, beStorage :: A.Value -> IO (Either String A.Value)
|
||||
-- | 取一份整库快照,当事务的起点
|
||||
-- ^ 原样转发一条存储请求
|
||||
, beSnapshot :: IO (Either String Database)
|
||||
-- | 事务提交:把一批写操作交给存储层应用
|
||||
-- ^ 取一份整库快照,当事务的起点
|
||||
, beApplyTransaction :: [TxnOp] -> IO (Either String ())
|
||||
-- ^ 事务提交:把一批写操作交给存储层应用
|
||||
}
|
||||
|
||||
-- | 按给定列名取列,固定列顺序
|
||||
@@ -79,7 +90,7 @@ columnsFromStatement db stmt = case prepare db stmt of
|
||||
Left _ -> []
|
||||
Right resolved -> columnsOf resolved
|
||||
where
|
||||
-- | 从已准备的语句提取输出列名
|
||||
-- \| 从已准备的语句提取输出列名
|
||||
columnsOf (SelectDistinct q) = columnsOf q
|
||||
columnsOf Select{selectItems = items} = map fst items
|
||||
columnsOf ShowDomains = ["domain", "base_type"]
|
||||
@@ -88,47 +99,75 @@ columnsFromStatement db stmt = case prepare db stmt of
|
||||
-- | 走本地存储链路的后端
|
||||
ipcBackend :: IO Backend
|
||||
ipcBackend = do
|
||||
let backend current = Backend
|
||||
{ beStatement = runIpc current
|
||||
, beCatalog = do
|
||||
response <- sendRequest (ReqInDatabase current ReqListCatalog)
|
||||
pure $ case response of
|
||||
RespCatalog infos -> Right (filter (not . isDomainTable . tiTable) infos)
|
||||
RespError err -> Left err
|
||||
_ -> Left "unexpected catalog response"
|
||||
, bePing = do
|
||||
resp <- sendRequest ReqPing
|
||||
pure (case resp of RespPong -> True; _ -> False)
|
||||
, beAccounts = \req -> do
|
||||
response <- sendRequest req
|
||||
pure $ case response of
|
||||
RespAccounts accounts -> Right accounts
|
||||
RespError err -> Left err
|
||||
_ -> Left "unexpected account response"
|
||||
, beWithDatabase = backend
|
||||
, beDatabases = do
|
||||
response <- sendRequest (ReqDatabase "list_databases" "")
|
||||
pure $ case response of
|
||||
RespTables names -> Right names
|
||||
RespError err -> Left err
|
||||
_ -> Left "unexpected database response"
|
||||
, beStorage = runRaw
|
||||
, beSnapshot = runIPCStorageIn snapshot (sessionEnv current)
|
||||
, beApplyTransaction = \ops -> do
|
||||
response <- sendRequest (ReqInDatabase current (ReqApplyTransaction ops))
|
||||
pure $ case response of
|
||||
RespOk -> Right ()
|
||||
RespError err -> Left err
|
||||
_ -> Left "unexpected transaction response"
|
||||
}
|
||||
snapshots <- newMVar Nothing
|
||||
let backend current =
|
||||
Backend
|
||||
{ beConcurrentTransactions = True
|
||||
, beBeginTransaction = \serializable -> do
|
||||
response <- sendRequest (ReqInDatabase current (ReqMvccBegin serializable))
|
||||
pure $ case response of
|
||||
RespMvccSnapshot token infos rows _ -> (,) (Just token) <$> snapshotDatabase infos rows
|
||||
RespError err -> Left err
|
||||
_ -> Left "unexpected MVCC begin response"
|
||||
, beStageTransaction = \token ops dependencies -> transactionRequest current (ReqMvccLock token ops dependencies)
|
||||
, bePreviewTransaction = \token -> do
|
||||
response <- sendRequest (ReqInDatabase current (ReqMvccPreview token))
|
||||
pure $ case response of
|
||||
RespDomainSnapshot infos rows checkpoint -> (,) checkpoint <$> snapshotDatabase infos rows
|
||||
RespError err -> Left err
|
||||
_ -> Left "unexpected MVCC preview response"
|
||||
, beCommitTransaction = \token checkpoint -> transactionRequest current (ReqMvccCommit token checkpoint)
|
||||
, beRollbackTransaction = transactionRequest current . ReqMvccRollback
|
||||
, beStatement = runIpc current
|
||||
, beCatalog = do
|
||||
response <- sendRequest (ReqInDatabase current ReqListCatalog)
|
||||
pure $ case response of
|
||||
RespCatalog infos -> Right (filter (not . isDomainTable . tiTable) infos)
|
||||
RespError err -> Left err
|
||||
_ -> Left "unexpected catalog response"
|
||||
, bePing = do
|
||||
resp <- sendRequest ReqPing
|
||||
pure (case resp of RespPong -> True; _ -> False)
|
||||
, beAccounts = \req -> do
|
||||
response <- sendRequest req
|
||||
pure $ case response of
|
||||
RespAccounts accounts -> Right accounts
|
||||
RespError err -> Left err
|
||||
_ -> Left "unexpected account response"
|
||||
, beWithDatabase = backend
|
||||
, beDatabases = do
|
||||
response <- sendRequest (ReqDatabase "list_databases" "")
|
||||
pure $ case response of
|
||||
RespTables names -> Right names
|
||||
RespError err -> Left err
|
||||
_ -> Left "unexpected database response"
|
||||
, beStorage = runRaw
|
||||
, beSnapshot = modifyMVar snapshots $ \previous -> do
|
||||
let cached = case previous of Just (name, version) | name == current -> Just version; _ -> Nothing
|
||||
result <- databaseSnapshot cached (sessionEnv current)
|
||||
pure $ case result of
|
||||
Left err -> (previous, Left err)
|
||||
Right version -> (Just (current, version), Right (snd version))
|
||||
, beApplyTransaction = \ops -> do
|
||||
response <- sendRequest (ReqInDatabase current (ReqApplyTransaction ops))
|
||||
pure $ case response of
|
||||
RespOk -> Right ()
|
||||
RespError err -> Left err
|
||||
_ -> Left "unexpected transaction response"
|
||||
}
|
||||
-- 没有默认库:没选库时 current 是空串,任何请求都得先选库
|
||||
pure (backend "")
|
||||
where
|
||||
-- | 会话的存储环境:库名跟这条连接走
|
||||
-- 转发事务生命周期请求并保留存储错误。
|
||||
transactionRequest current request = do
|
||||
response <- sendRequest (ReqInDatabase current request)
|
||||
pure $ case response of RespOk -> Right (); RespError err -> Left err; _ -> Left "unexpected MVCC response"
|
||||
|
||||
-- \| 会话的存储环境:库名跟这条连接走
|
||||
sessionEnv :: String -> Env
|
||||
sessionEnv current = envForDatabase (if null current then Nothing else Just current)
|
||||
|
||||
-- | 转发一条存储请求并解析响应
|
||||
-- \| 转发一条存储请求并解析响应
|
||||
runRaw :: A.Value -> IO (Either String A.Value)
|
||||
runRaw payload = do
|
||||
response <- sendRawRequest (BL.toStrict (A.encode payload))
|
||||
@@ -138,7 +177,7 @@ ipcBackend = do
|
||||
Left err -> Left ("storage response is not valid JSON: " ++ err)
|
||||
Right value -> Right value
|
||||
|
||||
-- | 走存储链路跑一条语句:表名不再补当前库前缀
|
||||
-- \| 走存储链路跑一条语句:表名不再补当前库前缀
|
||||
-- 引擎侧的数据字典按当前库过滤之后键是裸表名,补前缀会让查表落空;
|
||||
-- 库名由 database 字段随请求带走,存储层自己按它分发。
|
||||
runIpc :: String -> String -> IO (Either String StatementResult)
|
||||
@@ -166,7 +205,13 @@ memoryBackend = memoryBackendWith False
|
||||
memoryBackendWith :: Bool -> String -> MVar Database -> Backend
|
||||
memoryBackendWith trusted name ref =
|
||||
Backend
|
||||
{ beStatement = \sql -> case parseStatement sql >>= scopeDdl (if trusted then systemDatabaseName else name) of
|
||||
{ beConcurrentTransactions = False
|
||||
, beBeginTransaction = \_ -> fmap (fmap (Nothing,)) (beSnapshot (memoryBackend name ref))
|
||||
, beStageTransaction = \_ _ _ -> pure (Left "MVCC unavailable for memory backend")
|
||||
, bePreviewTransaction = \_ -> pure (Left "MVCC unavailable for memory backend")
|
||||
, beCommitTransaction = \_ _ -> pure (Left "MVCC unavailable for memory backend")
|
||||
, beRollbackTransaction = \_ -> pure (Left "MVCC unavailable for memory backend")
|
||||
, beStatement = \sql -> case parseStatement sql >>= scopeDdl (if trusted then systemDatabaseName else name) of
|
||||
Left e -> pure (Left e)
|
||||
-- 内存夹具也有"库"的概念:USE 只能切到它有的两个名字(自己 + system),
|
||||
-- 与 beDatabases 口径一致;引擎的内存存储不实现库管理,所以拦在这里。
|
||||
@@ -200,31 +245,31 @@ memoryBackendWith trusted name ref =
|
||||
Right db' -> pure (mergeScoped db db', Right ())
|
||||
}
|
||||
where
|
||||
-- | 按视角过滤库里的表
|
||||
scoped db = [(lastTablePart key, table {tableName = lastTablePart key}) | (key, table) <- db, inScope key, domainKey key || not (hidden key)]
|
||||
-- | 限定键只属于指定工作库
|
||||
-- \| 按视角过滤库里的表
|
||||
scoped db = [(lastTablePart key, table{tableName = lastTablePart key}) | (key, table) <- db, inScope key, domainKey key || not (hidden key)]
|
||||
-- \| 限定键只属于指定工作库
|
||||
inScope key = case break (== '.') key of
|
||||
(_, []) -> True
|
||||
(database, _) -> T.toLower (T.pack database) == T.toLower (T.pack (if trusted then systemDatabaseName else name))
|
||||
-- | 该视角下要隐藏的表
|
||||
-- \| 该视角下要隐藏的表
|
||||
hidden = (if trusted then blocked else internalTable) . lastTablePart
|
||||
-- | 判断裸名或限定名是否为类型目录
|
||||
-- \| 判断裸名或限定名是否为类型目录
|
||||
domainKey = isDomainTable . lastTablePart
|
||||
-- | 回写当前库并保留其他库和内部目录
|
||||
-- \| 回写当前库并保留其他库和内部目录
|
||||
mergeScoped original changed =
|
||||
filter (\(key, _) -> not (inScope key) || (hidden key && not (domainKey key))) original
|
||||
++ [(storageKey original key, table) | (key, table) <- changed]
|
||||
-- | 保留夹具中的原始数据库限定键
|
||||
-- \| 保留夹具中的原始数据库限定键
|
||||
storageKey original key = case [saved | (saved, _) <- original, inScope saved, lastTablePart saved == key] of
|
||||
saved : _ -> saved
|
||||
[] | any (\(saved, _) -> inScope saved && '.' `elem` saved) original -> (if trusted then systemDatabaseName else name) ++ "." ++ key
|
||||
[] -> key
|
||||
-- | 取结果列名,空结果回落到解析
|
||||
-- \| 取结果列名,空结果回落到解析
|
||||
colsOf db stmt rows = case rows of
|
||||
[] -> columnsFromStatement db stmt
|
||||
(r : _) -> map fst r
|
||||
|
||||
-- | 按目标库名挑视角
|
||||
-- \| 按目标库名挑视角
|
||||
withDatabase target
|
||||
| target == systemDatabaseName = memoryBackendWith True name ref
|
||||
| target == name = memoryBackendWith False name ref
|
||||
@@ -233,8 +278,9 @@ memoryBackendWith trusted name ref =
|
||||
|
||||
-- 更新内存夹具的稳定表编号。
|
||||
syncMemoryObjects :: String -> Database -> Database
|
||||
syncMemoryObjects database db = (registry, Table registry [("name", TStr), ("id", TInt)] records Nothing Nothing Nothing)
|
||||
: map identify (filter ((/= registry) . fst) db)
|
||||
syncMemoryObjects database db =
|
||||
(registry, Table registry [("name", TStr), ("id", TInt)] records Nothing Nothing Nothing)
|
||||
: map identify (filter ((/= registry) . fst) db)
|
||||
where
|
||||
registry = "__system_object_ids"
|
||||
previous = [(key, identifier) | row <- maybe [] tableRows (lookup registry db), Just (VStr key) <- [lookup "name" row], Just (VInt identifier) <- [lookup "id" row]]
|
||||
@@ -242,10 +288,19 @@ syncMemoryObjects database db = (registry, Table registry [("name", TStr), ("id"
|
||||
retained = [(key, identifier) | (key, identifier) <- previous, key `elem` keys]
|
||||
high = maximum (0 : map snd previous)
|
||||
added = zip [key | key <- keys, key `notElem` map fst retained] [high + 1 ..]
|
||||
-- | 将稳定目录编号附到表定义
|
||||
-- \| 将稳定目录编号附到表定义
|
||||
identify (key, table) = case lookup (if '.' `elem` key then key else database ++ "." ++ key) (retained ++ added) of
|
||||
Just identifier -> (key, table {tableIdentity = Just (TableIdentity
|
||||
(if "system." `isPrefixOf` key || ('.' `notElem` key && database == "system") then 2 else 1) (fromIntegral identifier))})
|
||||
Just identifier ->
|
||||
( key
|
||||
, table
|
||||
{ tableIdentity =
|
||||
Just
|
||||
( TableIdentity
|
||||
(if "system." `isPrefixOf` key || ('.' `notElem` key && database == "system") then 2 else 1)
|
||||
(fromIntegral identifier)
|
||||
)
|
||||
}
|
||||
)
|
||||
Nothing -> (key, table)
|
||||
records = [[("name", VStr key), ("id", VInt identifier)] | (key, identifier) <- ("__high_water", high + length added) : retained ++ added]
|
||||
|
||||
@@ -267,92 +322,108 @@ memorySecurityRequest database ref payload = modifyMVar ref $ \original -> do
|
||||
operation db = case field "method" of
|
||||
Just "resolve_object" -> do
|
||||
selected <- maybe (Left "missing database") Right (field "database")
|
||||
if selected `notElem` [T.pack database, "system"] then Left "unknown database" else do
|
||||
identifier <- case field "table" of
|
||||
Nothing -> Right A.Null
|
||||
Just table -> case [number | row <- maybe [] tableRows (lookup "__system_object_ids" db), lookup "name" row == Just (VStr (T.unpack (selected <> "." <> table))), Just (VInt number) <- [lookup "id" row]] of
|
||||
[number] -> Right (A.toJSON number)
|
||||
_ -> Left "unknown table"
|
||||
Right (db, A.object ["status" A..= ("object" :: Text), "database_id" A..= (if selected == "system" then (2 :: Int) else 1), "object_id" A..= identifier])
|
||||
if selected `notElem` [T.pack database, "system"]
|
||||
then Left "unknown database"
|
||||
else do
|
||||
identifier <- case field "table" of
|
||||
Nothing -> Right A.Null
|
||||
Just table -> case [number | row <- maybe [] tableRows (lookup "__system_object_ids" db), lookup "name" row == Just (VStr (T.unpack (selected <> "." <> table))), Just (VInt number) <- [lookup "id" row]] of
|
||||
[number] -> Right (A.toJSON number)
|
||||
_ -> Left "unknown table"
|
||||
Right (db, A.object ["status" A..= ("object" :: Text), "database_id" A..= (if selected == "system" then (2 :: Int) else 1), "object_id" A..= identifier])
|
||||
Just "object_acl_read" -> Right (db, A.object ["status" A..= ("rows" :: Text), "rows" A..= [A.object ["id" A..= (1 :: Int), "payload" A..= value] | row <- maybe [] tableRows (lookup acl db), Just (VStr value) <- [lookup "payload" row]]])
|
||||
Just "object_acl_replace" -> do
|
||||
let previous = case lookup acl db of Nothing -> Nothing; Just table -> case tableRows table of [[("payload", VStr value)]] -> Just (T.pack value); _ -> Just "invalid ACL"
|
||||
if previous /= field "expected" then Left "object ACL changed concurrently" else do
|
||||
value <- maybe (Left "missing ACL payload") Right (field "payload")
|
||||
Right ((acl, Table acl [("payload", TStr)] [[("payload", VStr (T.unpack value))]] Nothing Nothing Nothing) : filter ((/= acl) . fst) db, A.object ["status" A..= ("ok" :: Text)])
|
||||
if previous /= field "expected"
|
||||
then Left "object ACL changed concurrently"
|
||||
else do
|
||||
value <- maybe (Left "missing ACL payload") Right (field "payload")
|
||||
Right ((acl, Table acl [("payload", TStr)] [[("payload", VStr (T.unpack value))]] Nothing Nothing Nothing) : filter ((/= acl) . fst) db, A.object ["status" A..= ("ok" :: Text)])
|
||||
_ -> Left "storage is not available in this backend"
|
||||
acl = "__system_object_acl"
|
||||
|
||||
-- | 把后端的语句与字典都改成同一条错误
|
||||
unavailableDatabase :: String -> Backend -> Backend
|
||||
unavailableDatabase message backend = backend
|
||||
{ beStatement = \_ -> pure (Left message)
|
||||
, beCatalog = pure (Left message)
|
||||
, beSnapshot = pure (Left message)
|
||||
, beApplyTransaction = \_ -> pure (Left message)
|
||||
}
|
||||
unavailableDatabase message backend =
|
||||
backend
|
||||
{ beStatement = \_ -> pure (Left message)
|
||||
, beCatalog = pure (Left message)
|
||||
, beSnapshot = pure (Left message)
|
||||
, beApplyTransaction = \_ -> pure (Left message)
|
||||
}
|
||||
|
||||
-- | 未选库时的视角:只放行不碰表的语句
|
||||
noDatabaseBackend :: Backend -> Backend
|
||||
noDatabaseBackend backend = backend
|
||||
{ beStatement = \sql -> case parseStatement sql of
|
||||
Left e -> pure (Left e)
|
||||
Right stmt
|
||||
| statementNeedsDatabase stmt -> pure (Left "no database selected")
|
||||
| otherwise -> beStatement backend sql
|
||||
, beCatalog = pure (Left "no database selected")
|
||||
, beSnapshot = pure (Left "no database selected")
|
||||
, beApplyTransaction = \_ -> pure (Left "no database selected")
|
||||
}
|
||||
noDatabaseBackend backend =
|
||||
backend
|
||||
{ beStatement = \sql -> case parseStatement sql of
|
||||
Left e -> pure (Left e)
|
||||
Right stmt
|
||||
| statementNeedsDatabase stmt -> pure (Left "no database selected")
|
||||
| otherwise -> beStatement backend sql
|
||||
, beCatalog = pure (Left "no database selected")
|
||||
, beSnapshot = pure (Left "no database selected")
|
||||
, beApplyTransaction = \_ -> pure (Left "no database selected")
|
||||
}
|
||||
|
||||
-- | 把一批事务写操作应用到内存库:先按 id 删,再按追加写
|
||||
applyTransactionMemory :: [TxnOp] -> Database -> Either String Database
|
||||
applyTransactionMemory ops db = foldl step (Right db) ops
|
||||
where
|
||||
-- 顺序预演每个事务操作。
|
||||
step acc op = do
|
||||
current <- acc
|
||||
case op of
|
||||
TxnDelete name keys -> do
|
||||
table <- lookupTable current name
|
||||
pure (put (name, table {tableRows = [r | r <- tableRows table, not (doomed keys r)]}) current)
|
||||
pure (put (name, table{tableRows = deleteFirst (Set.fromList keys) (tableRows table)}) current)
|
||||
TxnUpsert name rows -> do
|
||||
table <- lookupTable current name
|
||||
pure (put (name, table {tableRows = tableRows table ++ rows}) current)
|
||||
pure (put (name, table{tableRows = tableRows table ++ rows}) current)
|
||||
TxnReplace name rows -> do
|
||||
table <- lookupTable current name
|
||||
pure (put (name, table {tableRows = rows}) current)
|
||||
pure (put (name, table{tableRows = rows}) current)
|
||||
|
||||
-- | 换掉一张表,其余保持原顺序
|
||||
-- \| 换掉一张表,其余保持原顺序
|
||||
put entry = map (\one@(name, _) -> if name == fst entry then entry else one)
|
||||
|
||||
-- | 这一行要不要删:按行里的 id 列比
|
||||
doomed keys r = case lookup "id" r of
|
||||
Just (VInt k) -> k `elem` keys
|
||||
_ -> False
|
||||
-- 与存储一致,每个删除键只删除首行。
|
||||
deleteFirst _ [] = []
|
||||
deleteFirst keys (row : rows) = case lookup "id" row of
|
||||
Just (VInt key) | Set.member key keys -> deleteFirst (Set.delete key keys) rows
|
||||
_ -> row : deleteFirst keys rows
|
||||
|
||||
-- | 语句里出现的表
|
||||
statementTables :: Statement -> [Text]
|
||||
statementTables stmt = nub (case stmt of
|
||||
SelectDistinct q -> statementTables q
|
||||
Select{} ->
|
||||
tableRefsOf (selectFrom stmt)
|
||||
++ concatMap exprTables (maybeToList (selectWhere stmt) ++ maybeToList (selectHaving stmt)
|
||||
++ map snd (selectItems stmt) ++ map Col (selectGroupBy stmt)
|
||||
++ mapMaybe (orderKeyExpression . fst) (selectOrderBy stmt))
|
||||
Insert table _ rows -> T.pack table : concatMap exprTables (concat rows)
|
||||
Update table assigns cond -> T.pack table : concatMap exprTables (map snd assigns ++ maybeToList cond)
|
||||
Delete table cond -> T.pack table : concatMap exprTables (maybeToList cond)
|
||||
CreateTable table _ -> [T.pack table]
|
||||
DropTable table -> [T.pack table]
|
||||
CreateIndex table _ -> [T.pack table]
|
||||
DropIndex table _ -> [T.pack table]
|
||||
DropColumn table _ -> [T.pack table]
|
||||
AddColumn table _ -> [T.pack table]
|
||||
RenameColumn table _ _ -> [T.pack table]
|
||||
AlterColumnType table _ _ -> [T.pack table]
|
||||
AlterColumnDefault table _ _ -> [T.pack table]
|
||||
AlterColumnNull table _ _ -> [T.pack table]
|
||||
_ -> [])
|
||||
statementTables stmt =
|
||||
nub
|
||||
( case stmt of
|
||||
SelectDistinct q -> statementTables q
|
||||
Select{} ->
|
||||
tableRefsOf (selectFrom stmt)
|
||||
++ concatMap
|
||||
exprTables
|
||||
( maybeToList (selectWhere stmt)
|
||||
++ maybeToList (selectHaving stmt)
|
||||
++ map snd (selectItems stmt)
|
||||
++ map Col (selectGroupBy stmt)
|
||||
++ mapMaybe (orderKeyExpression . fst) (selectOrderBy stmt)
|
||||
)
|
||||
Insert table _ rows -> T.pack table : concatMap exprTables (concat rows)
|
||||
Update table assigns cond -> T.pack table : concatMap exprTables (map snd assigns ++ maybeToList cond)
|
||||
Delete table cond -> T.pack table : concatMap exprTables (maybeToList cond)
|
||||
CreateTable table _ -> [T.pack table]
|
||||
DropTable table -> [T.pack table]
|
||||
CreateIndex table _ -> [T.pack table]
|
||||
DropIndex table _ -> [T.pack table]
|
||||
DropColumn table _ -> [T.pack table]
|
||||
AddColumn table _ -> [T.pack table]
|
||||
RenameColumn table _ _ -> [T.pack table]
|
||||
AlterColumnType table _ _ -> [T.pack table]
|
||||
AlterColumnDefault table _ _ -> [T.pack table]
|
||||
AlterColumnNull table _ _ -> [T.pack table]
|
||||
_ -> []
|
||||
)
|
||||
|
||||
-- | 语句需不需要一个当前库:出现的表里有没写库名的就必须要
|
||||
statementNeedsDatabase :: Statement -> Bool
|
||||
@@ -368,6 +439,7 @@ statementNeedsDatabase stmt = case stmt of
|
||||
AlterColumnDefault{} -> True
|
||||
AlterColumnNull{} -> True
|
||||
CreateDomain{} -> True
|
||||
AlterDomain{} -> True
|
||||
DropDomain{} -> True
|
||||
ShowDomains -> True
|
||||
_ -> not (null (statementTables stmt))
|
||||
@@ -387,14 +459,14 @@ scopeDdl current statement = case statement of
|
||||
AlterColumnNull table column nullable -> (\key -> AlterColumnNull key column nullable) <$> target table
|
||||
_ -> statement <$ mapM_ checkReadTarget (statementTables statement)
|
||||
where
|
||||
-- | 数据引用限定在当前工作库
|
||||
-- \| 数据引用限定在当前工作库
|
||||
checkReadTarget table
|
||||
| null current = Left "no database selected"
|
||||
| otherwise = case T.breakOn "." table of
|
||||
(_, suffix) | T.null suffix -> Right ()
|
||||
(database, _) | T.toLower database == T.toLower (T.pack current) -> Right ()
|
||||
_ -> Left ("table is outside the current database: " ++ T.unpack table)
|
||||
-- | 校验库限定名并取当前库中的表名
|
||||
-- \| 校验库限定名并取当前库中的表名
|
||||
target table
|
||||
| null current = Left "no database selected"
|
||||
| otherwise = case break (== '.') table of
|
||||
@@ -441,7 +513,7 @@ exprTables expr = case expr of
|
||||
MaxOf a -> exprTables a
|
||||
_ -> []
|
||||
where
|
||||
-- | 两侧都取表名
|
||||
-- \| 两侧都取表名
|
||||
both a b = exprTables a ++ exprTables b
|
||||
|
||||
-- | 一张表的线上信息(内存实现:没有索引,统计与直方图现算)
|
||||
@@ -458,10 +530,10 @@ tableInfoOf (name, tbl) =
|
||||
, tiHistograms = [(c, h) | (c, _) <- tableCols tbl, Just h <- [histogramOf (columnValues c)]]
|
||||
}
|
||||
where
|
||||
-- | 一列的所有值
|
||||
-- \| 一列的所有值
|
||||
columnValues c = [v | r <- tableRows tbl, Just v <- [lookup c r]]
|
||||
|
||||
-- | 一列的不同值个数
|
||||
-- \| 一列的不同值个数
|
||||
distinctOf c = length (nub (columnValues c))
|
||||
|
||||
-- | 本地列类型转线上字符串(和 IPC 那边同一套写法)
|
||||
@@ -551,21 +623,36 @@ memoryAccounts db stamp req = do
|
||||
_ -> Nothing
|
||||
allowed = case command of ReqAccountsList -> True; ReqAccountCreate{} -> True; ReqAccountReset{} -> True; ReqAccountDrop{} -> True; ReqIdentityAlter _ _ Nothing _ Nothing Nothing -> True; _ -> False
|
||||
if not allowed || any (\a -> target == Just (accountUser a) && (accountIsSuperuser a || accountSystemCatalogManager a || accountAllowSudoAuth a)) accounts
|
||||
then Left "superuser required to manage privileged identities" else memoryAccounts db stamp command
|
||||
then Left "superuser required to manage privileged identities"
|
||||
else memoryAccounts db stamp command
|
||||
ReqAccountsList -> Right accounts
|
||||
ReqIdentityInitialize admin
|
||||
| any (`elem` map accountUser accounts) legacyNames || length (nub legacyNames) /= length legacyNames -> Left "identity name collision"
|
||||
| otherwise -> Right ([if legacyIdentity a && accountUser a == T.toLower admin then a{accountIsSuperuser = True} else a | a <- accounts] ++
|
||||
[Account (highWater + offset) role "" 1 stamp Nothing False False True False False | (offset, role) <- zip [1..] legacyNames])
|
||||
| otherwise ->
|
||||
Right
|
||||
( [if legacyIdentity a && accountUser a == T.toLower admin then a{accountIsSuperuser = True} else a | a <- accounts]
|
||||
++ [Account (highWater + offset) role "" 1 stamp Nothing False False True False False | (offset, role) <- zip [1 ..] legacyNames]
|
||||
)
|
||||
ReqRoleCreate u
|
||||
| any ((== T.toLower u) . accountUser) accounts -> Left "identity already exists"
|
||||
| otherwise -> Right (accounts ++ [Account (highWater + 1) (T.toLower u) "" 1 stamp Nothing False False True False False])
|
||||
ReqIdentityAlter u login super enabled manager sudo -> case filter ((== T.toLower u) . accountUser) accounts of
|
||||
[_] -> preserveSuper [if accountUser a == T.toLower u then a{accountCanLogin = fromMaybe (accountCanLogin a) login,
|
||||
accountIsSuperuser = fromMaybe (accountIsSuperuser a) super, accountEnabled = fromMaybe (accountEnabled a) enabled,
|
||||
accountSystemCatalogManager = fromMaybe (accountSystemCatalogManager a) manager,
|
||||
accountAllowSudoAuth = fromMaybe (accountAllowSudoAuth a) sudo,
|
||||
accountRevision = accountRevision a + 1} else a | a <- accounts] accounts
|
||||
[_] ->
|
||||
preserveSuper
|
||||
[ if accountUser a == T.toLower u
|
||||
then
|
||||
a
|
||||
{ accountCanLogin = fromMaybe (accountCanLogin a) login
|
||||
, accountIsSuperuser = fromMaybe (accountIsSuperuser a) super
|
||||
, accountEnabled = fromMaybe (accountEnabled a) enabled
|
||||
, accountSystemCatalogManager = fromMaybe (accountSystemCatalogManager a) manager
|
||||
, accountAllowSudoAuth = fromMaybe (accountAllowSudoAuth a) sudo
|
||||
, accountRevision = accountRevision a + 1
|
||||
}
|
||||
else a
|
||||
| a <- accounts
|
||||
]
|
||||
accounts
|
||||
_ -> Left "unknown account"
|
||||
ReqAccountCreate u h
|
||||
| any ((== T.toLower u) . accountUser) accounts -> Left "account already exists"
|
||||
@@ -581,51 +668,51 @@ memoryAccounts db stamp req = do
|
||||
_ -> preserveSuper [x | x <- accounts, accountUser x /= T.toLower u] accounts
|
||||
_ -> Left "invalid account request"
|
||||
where
|
||||
-- | 判断旧格式身份是否缺少属性
|
||||
-- \| 判断旧格式身份是否缺少属性
|
||||
legacyIdentity account = any (legacyRow (accountUser account)) (maybe [] tableRows (lookup usersTable db))
|
||||
-- | 检查旧身份的元数据版本
|
||||
-- \| 检查旧身份的元数据版本
|
||||
legacyRow user row = case lookup "account" row of
|
||||
Just (VStr encoded) -> case A.decode (BL.pack encoded) of
|
||||
Just (A.Object fields) -> KM.lookup "user" fields == Just (A.String user) && not (KM.member "is_superuser" fields)
|
||||
_ -> False
|
||||
_ -> False
|
||||
-- | 读取旧格式账号
|
||||
-- \| 读取旧格式账号
|
||||
decodeAccount row = case lookup "account" row of
|
||||
Just (VStr encoded) -> A.eitherDecode (BL.pack encoded)
|
||||
_ -> Left "invalid account record"
|
||||
-- | 合并身份与口令
|
||||
-- \| 合并身份与口令
|
||||
decodeCredential row = do
|
||||
identity <- decodeAccount row
|
||||
case [hash | credential <- maybe [] tableRows (lookup usersTable db), lookup "id" credential == Just (VInt (fromInteger (accountId identity))), Just (VStr hash) <- [lookup "password_hash" credential]] of
|
||||
[hash] -> Right identity{accountHash = T.pack hash}
|
||||
_ -> Left "invalid identity credential"
|
||||
-- | 保留启用的登录管理员
|
||||
-- \| 保留启用的登录管理员
|
||||
preserveSuper next old
|
||||
| any activeSuper old && not (any activeSuper next) = Left "the last enabled login superuser cannot be removed"
|
||||
| otherwise = Right next
|
||||
-- | 判断可登录管理员
|
||||
-- \| 判断可登录管理员
|
||||
activeSuper a = accountEnabled a && accountCanLogin a && accountIsSuperuser a
|
||||
|
||||
-- | 分开保存内存身份与口令并清理授权
|
||||
storeMemoryIdentities :: Database -> [Account] -> [Account] -> Database
|
||||
storeMemoryIdentities db previous accounts =
|
||||
(usersTable, Table usersTable [("id", TInt), ("password_hash", TStr)] credentials Nothing Nothing Nothing) :
|
||||
("__system_identities", Table "__system_identities" [("account", TStr)] identities Nothing Nothing Nothing) :
|
||||
("__system_identity_sequence", Table "__system_identity_sequence" [("id", TInt)] [[("id", VInt highWater)]] Nothing Nothing Nothing) :
|
||||
[(name, clean name table) | (name, table) <- db, name `notElem` [usersTable, "__system_identities", "__system_identity_sequence"]]
|
||||
(usersTable, Table usersTable [("id", TInt), ("password_hash", TStr)] credentials Nothing Nothing Nothing)
|
||||
: ("__system_identities", Table "__system_identities" [("account", TStr)] identities Nothing Nothing Nothing)
|
||||
: ("__system_identity_sequence", Table "__system_identity_sequence" [("id", TInt)] [[("id", VInt highWater)]] Nothing Nothing Nothing)
|
||||
: [(name, clean name table) | (name, table) <- db, name `notElem` [usersTable, "__system_identities", "__system_identity_sequence"]]
|
||||
where
|
||||
-- | 保存口令与身份编号
|
||||
-- \| 保存口令与身份编号
|
||||
credentials = [[("id", VInt (fromInteger (accountId a))), ("password_hash", VStr (T.unpack (accountHash a)))] | a <- accounts]
|
||||
-- | 保存不含口令的身份记录
|
||||
-- \| 保存不含口令的身份记录
|
||||
identities = [[("account", VStr (BL.unpack (A.encode a{accountHash = ""})))] | a <- accounts]
|
||||
-- | 保留已用编号上界
|
||||
-- \| 保留已用编号上界
|
||||
highWater = maximum (0 : map (fromInteger . accountId) accounts ++ [n | row <- maybe [] tableRows (lookup "__system_identity_sequence" db), Just (VInt n) <- [lookup "id" row]])
|
||||
-- | 清理旧目录与失效的授权边
|
||||
-- \| 清理旧目录与失效的授权边
|
||||
clean name table
|
||||
| name == rolesTable = table{tableRows = []}
|
||||
| name `elem` [grantsTable, optionsTable, membersTable] = table{tableRows = filter active (tableRows table)}
|
||||
| otherwise = table
|
||||
-- | 识别已经删除的身份
|
||||
-- \| 识别已经删除的身份
|
||||
removed = [accountUser a | a <- previous, accountUser a `notElem` map accountUser accounts]
|
||||
-- | 排除已删除身份的授权和成员边
|
||||
-- \| 排除已删除身份的授权和成员边
|
||||
active row = not (any (\field -> case lookup field row of Just (VStr value) -> T.pack value `elem` removed; _ -> False) ["role", "member"])
|
||||
@@ -1,35 +1,35 @@
|
||||
module ChuSQL.Server.Catalog
|
||||
( Catalog
|
||||
, newCatalog
|
||||
, normalizeRole
|
||||
, normalizeUser
|
||||
, normalizeObject
|
||||
, withCatalog
|
||||
, withCatalogLock
|
||||
, ensureCatalog
|
||||
, eachAction
|
||||
, readRoleNames
|
||||
, readIdentities
|
||||
, insertRole
|
||||
, dropRole
|
||||
, readGrants
|
||||
, addGrant
|
||||
, removeGrant
|
||||
, readGrantOptions
|
||||
, addGrantOption
|
||||
, removeGrantOption
|
||||
, readMembers
|
||||
, addMember
|
||||
, removeMember
|
||||
) where
|
||||
module ChuSQL.Server.Catalog (
|
||||
Catalog,
|
||||
newCatalog,
|
||||
normalizeRole,
|
||||
normalizeUser,
|
||||
normalizeObject,
|
||||
withCatalog,
|
||||
withCatalogLock,
|
||||
ensureCatalog,
|
||||
eachAction,
|
||||
readRoleNames,
|
||||
readIdentities,
|
||||
insertRole,
|
||||
dropRole,
|
||||
readGrants,
|
||||
addGrant,
|
||||
removeGrant,
|
||||
readGrantOptions,
|
||||
addGrantOption,
|
||||
removeGrantOption,
|
||||
readMembers,
|
||||
addMember,
|
||||
removeMember,
|
||||
) where
|
||||
|
||||
import Control.Monad (void)
|
||||
import ChuSQL.Core.Model (Row, Value (..))
|
||||
import ChuSQL.Core.Protocol (Account (..), Request (..))
|
||||
import ChuSQL.Interface.Actions (escapeStringLiteral)
|
||||
import ChuSQL.Interface.Protocol (Grant (..))
|
||||
import ChuSQL.Server.Backend (Backend (..), StatementResult (..), grantsTable, membersTable, optionsTable, rolesTable)
|
||||
import Control.Concurrent.MVar (MVar, newMVar, withMVar)
|
||||
import Control.Monad (void)
|
||||
import Data.List (intercalate, isInfixOf)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
@@ -77,7 +77,7 @@ withCatalog catalog action = withCatalogLock catalog $ do
|
||||
ensureCatalog :: Catalog -> IO (Either String ())
|
||||
ensureCatalog catalog = eachAction tableDefs (createTable catalog)
|
||||
where
|
||||
-- | 三张系统表的定义
|
||||
-- \| 三张系统表的定义
|
||||
tableDefs =
|
||||
[ (rolesTable, "(name VARCHAR(64))")
|
||||
, (grantsTable, "(role VARCHAR(64), privilege VARCHAR(16), object VARCHAR(128))")
|
||||
@@ -191,7 +191,7 @@ grantOf row =
|
||||
|
||||
-- | 一行带 grant option 的授权记录
|
||||
optionOf :: Row -> Grant
|
||||
optionOf row = (grantOf row) {grantable = True}
|
||||
optionOf row = (grantOf row){grantable = True}
|
||||
|
||||
-- | 一行成员关系
|
||||
memberOf :: Row -> (Text, Text)
|
||||
@@ -200,8 +200,14 @@ memberOf row = (normalizeRole (cell "role" row), normalizeUser (cell "member" ro
|
||||
-- | 拼一条删授权的语句
|
||||
deleteGrantSql :: String -> Text -> Text -> Text -> String
|
||||
deleteGrantSql table role privilege object =
|
||||
"DELETE FROM " ++ table ++ " WHERE role = " ++ roleLiteral role
|
||||
++ " AND privilege = " ++ literal privilege ++ " AND object = " ++ literal object
|
||||
"DELETE FROM "
|
||||
++ table
|
||||
++ " WHERE role = "
|
||||
++ roleLiteral role
|
||||
++ " AND privilege = "
|
||||
++ literal privilege
|
||||
++ " AND object = "
|
||||
++ literal object
|
||||
|
||||
-- | 拼一条插入语句
|
||||
insertSql :: String -> [String] -> [Text] -> String
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
module ChuSQL.Server.Normalization (normalizationBudget, normalizationReport, schemaFingerprint) where
|
||||
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified ChuSQL.Core.Engine.Normalization as N
|
||||
import ChuSQL.Core.Model (Column (..), parseColumnType)
|
||||
import ChuSQL.Core.Protocol (SchemaColumn (..), valueToJSON)
|
||||
@@ -11,10 +10,11 @@ import Control.Concurrent.MVar
|
||||
import Control.Concurrent.QSem
|
||||
import Control.Exception (bracket_, evaluate, mask)
|
||||
import Crypto.Hash (Digest, SHA256, hash)
|
||||
import Data.Aeson (object, (.=))
|
||||
import qualified Data.Aeson as A
|
||||
import Data.Aeson ((.=), object)
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import Data.List (nub, sort)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
@@ -32,8 +32,9 @@ normalizationBudget :: IO (Either (Text, Text) A.Value) -> IO (Either (Text, Tex
|
||||
normalizationBudget action = mask $ \restore -> do
|
||||
let (queued, slots) = analysisSlots
|
||||
admitted <- modifyMVar queued $ \count -> pure (if count < 8 then (count + 1, True) else (count, False))
|
||||
if not admitted then pure (Left ("too_busy", "normalization queue is full")) else
|
||||
bracket_ (pure ()) (modifyMVar_ queued (pure . subtract 1)) $ do
|
||||
if not admitted
|
||||
then pure (Left ("too_busy", "normalization queue is full"))
|
||||
else bracket_ (pure ()) (modifyMVar_ queued (pure . subtract 1)) $ do
|
||||
result <- restore $ timeout 5000000 $ bracket_ (waitQSem slots) (signalQSem slots) $ do
|
||||
outcome <- action
|
||||
case outcome of
|
||||
@@ -45,8 +46,17 @@ normalizationBudget action = mask $ \restore -> do
|
||||
|
||||
-- 编码只影响语义的列信息。
|
||||
fingerprintColumn :: SchemaColumn -> A.Value
|
||||
fingerprintColumn c = A.toJSON [A.toJSON (scName c), A.toJSON (scType c), A.toJSON (scNullable c),
|
||||
maybe A.Null valueToJSON (scDefault c), A.toJSON (scAutoIncrement c), A.toJSON (scPrimaryKey c), A.toJSON (scUnique c), A.toJSON (scCheck c)]
|
||||
fingerprintColumn c =
|
||||
A.toJSON
|
||||
[ A.toJSON (scName c)
|
||||
, A.toJSON (scType c)
|
||||
, A.toJSON (scNullable c)
|
||||
, maybe A.Null valueToJSON (scDefault c)
|
||||
, A.toJSON (scAutoIncrement c)
|
||||
, A.toJSON (scPrimaryKey c)
|
||||
, A.toJSON (scUnique c)
|
||||
, A.toJSON (scCheck c)
|
||||
]
|
||||
|
||||
-- 将稳定编号与列结构绑定到 SHA256。
|
||||
schemaFingerprint :: ObjectIdentity -> [SchemaColumn] -> Text
|
||||
@@ -57,16 +67,29 @@ normalizationReport :: ObjectIdentity -> [SchemaColumn] -> Text -> Normalization
|
||||
normalizationReport identity columns view payload = do
|
||||
let fingerprint = schemaFingerprint identity columns
|
||||
if maybe False (/= identity) (nrExpectedObject payload) || maybe False (/= fingerprint) (nrExpectedSchema payload)
|
||||
then Left ("conflict", "normalization object or schema has changed") else Right ()
|
||||
then Left ("conflict", "normalization object or schema has changed")
|
||||
else Right ()
|
||||
schema <- mapM column columns
|
||||
let declaration = nrDeclaration payload
|
||||
axioms = N.Declaration (ndKeys declaration) [N.Dependency a b | NormalizationDependency a b <- ndDependencies declaration] (ndComplete declaration) (ndAtomic declaration)
|
||||
analysis <- either (Left . (,) "bad_request" . T.pack) Right (N.analyzeNormalization schema axioms)
|
||||
Right (object ["api_version" .= (1 :: Int), "database" .= nrDatabase payload, "table" .= last (T.splitOn "." (nrTable payload)),
|
||||
"object" .= identity, "schema_fingerprint" .= fingerprint, "view" .= view, "schema" .= columns,
|
||||
"declaration" .= canonicalDeclaration declaration, "candidate_keys" .= N.candidateKeys analysis,
|
||||
"findings" .= map findingJSON (N.findings analysis), "suggestions" .= map decompositionJSON (N.suggestions analysis),
|
||||
"conditional" .= True, "condition" .= ("Analysis is conditional on the declarations; SQL NULL, UNIQUE and bag semantics are not proofs of classical keys or lossless data migration." :: Text)])
|
||||
Right
|
||||
( object
|
||||
[ "api_version" .= (1 :: Int)
|
||||
, "database" .= nrDatabase payload
|
||||
, "table" .= last (T.splitOn "." (nrTable payload))
|
||||
, "object" .= identity
|
||||
, "schema_fingerprint" .= fingerprint
|
||||
, "view" .= view
|
||||
, "schema" .= columns
|
||||
, "declaration" .= canonicalDeclaration declaration
|
||||
, "candidate_keys" .= N.candidateKeys analysis
|
||||
, "findings" .= map findingJSON (N.findings analysis)
|
||||
, "suggestions" .= map decompositionJSON (N.suggestions analysis)
|
||||
, "conditional" .= True
|
||||
, "condition" .= ("Analysis is conditional on the declarations; SQL NULL, UNIQUE and bag semantics are not proofs of classical keys or lossless data migration." :: Text)
|
||||
]
|
||||
)
|
||||
where
|
||||
-- 还原列类型及声明约束而不读取数据。
|
||||
column c = case parseColumnType (scType c) of
|
||||
@@ -75,13 +98,21 @@ normalizationReport identity columns view payload = do
|
||||
|
||||
-- 规范化经验证的声明属性集合。
|
||||
canonicalDeclaration :: NormalizationDeclaration -> NormalizationDeclaration
|
||||
canonicalDeclaration d = d {ndKeys = sort (nub (map N.canonical (ndKeys d))),
|
||||
ndDependencies = [NormalizationDependency a b | (a, b) <- sort (nub [(N.canonical a, N.canonical b) | NormalizationDependency a b <- ndDependencies d])]}
|
||||
canonicalDeclaration d =
|
||||
d
|
||||
{ ndKeys = sort (nub (map N.canonical (ndKeys d)))
|
||||
, ndDependencies = [NormalizationDependency a b | (a, b) <- sort (nub [(N.canonical a, N.canonical b) | NormalizationDependency a b <- ndDependencies d])]
|
||||
}
|
||||
|
||||
-- 编码稳定范式标签及证据。
|
||||
findingJSON :: N.Finding -> A.Value
|
||||
findingJSON f = object ["normal_form" .= formTag (N.normalForm f), "verdict" .= verdictTag (N.verdict f),
|
||||
"witnesses" .= [NormalizationDependency a b | N.Dependency a b <- N.witnesses f], "condition" .= N.condition f]
|
||||
findingJSON f =
|
||||
object
|
||||
[ "normal_form" .= formTag (N.normalForm f)
|
||||
, "verdict" .= verdictTag (N.verdict f)
|
||||
, "witnesses" .= [NormalizationDependency a b | N.Dependency a b <- N.witnesses f]
|
||||
, "condition" .= N.condition f
|
||||
]
|
||||
|
||||
-- 编码一次分解的理论结论。
|
||||
decompositionJSON :: N.Decomposition -> A.Value
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module ChuSQL.Server.ObjectACL
|
||||
( objectAuthorize, objectGrant, objectRevoke, objectRevokeGrantOption, objectViews, objectClaim
|
||||
, objectSelfViews, objectReconcile, objectAffected, objectOwner, objectConnect, objectPrepareCreate, objectUsers
|
||||
) where
|
||||
module ChuSQL.Server.ObjectACL (
|
||||
objectAuthorize,
|
||||
objectGrant,
|
||||
objectRevoke,
|
||||
objectRevokeGrantOption,
|
||||
objectViews,
|
||||
objectClaim,
|
||||
objectSelfViews,
|
||||
objectReconcile,
|
||||
objectAffected,
|
||||
objectOwner,
|
||||
objectConnect,
|
||||
objectPrepareCreate,
|
||||
objectUsers,
|
||||
) where
|
||||
|
||||
import Data.Either (fromRight)
|
||||
import Control.Monad (void)
|
||||
import ChuSQL.Core.Protocol (Account (..), TableInfo (..))
|
||||
import ChuSQL.Interface.Protocol (Grant (..))
|
||||
import ChuSQL.Server.Accounts (Principal, principalIsRoot, principalName)
|
||||
import ChuSQL.Server.Backend (Backend (..))
|
||||
import ChuSQL.Server.Catalog (Catalog, readIdentities, readMembers, readGrants, readGrantOptions)
|
||||
import ChuSQL.Server.Catalog (Catalog, readGrantOptions, readGrants, readIdentities, readMembers)
|
||||
import Control.Monad (void)
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.Aeson.Key as K
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||
import Data.Either (fromRight)
|
||||
import Data.List (nub)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
@@ -26,35 +37,40 @@ import GHC.Generics (Generic)
|
||||
|
||||
-- 稳定对象权限目录:保存所有者、授予来源并计算有效授权链。
|
||||
|
||||
data Target = Target { targetDatabase :: Text, targetTable :: Maybe Text, targetKind :: Text, targetName :: Text }
|
||||
data Target = Target {targetDatabase :: Text, targetTable :: Maybe Text, targetKind :: Text, targetName :: Text}
|
||||
deriving (Eq, Show, Generic)
|
||||
instance ToJSON Target
|
||||
instance FromJSON Target
|
||||
|
||||
data Edge = Edge { edgeRole :: Text, edgeGrantor :: Text, edgeTarget :: Target, edgePrivilege :: Text, edgeOption :: Bool, edgeRoot :: Bool }
|
||||
data Edge = Edge {edgeRole :: Text, edgeGrantor :: Text, edgeTarget :: Target, edgePrivilege :: Text, edgeOption :: Bool, edgeRoot :: Bool}
|
||||
deriving (Eq, Show, Generic)
|
||||
instance ToJSON Edge
|
||||
instance FromJSON Edge
|
||||
|
||||
data Owner = Owner { ownerRole :: Text, ownerTarget :: Target }
|
||||
data Owner = Owner {ownerRole :: Text, ownerTarget :: Target}
|
||||
deriving (Eq, Show, Generic)
|
||||
instance ToJSON Owner
|
||||
instance FromJSON Owner
|
||||
|
||||
data Pending = Pending { pendingRole :: Text, pendingDatabase :: Text, pendingName :: Text }
|
||||
data Pending = Pending {pendingRole :: Text, pendingDatabase :: Text, pendingName :: Text}
|
||||
deriving (Eq, Show, Generic)
|
||||
instance ToJSON Pending
|
||||
instance FromJSON Pending
|
||||
|
||||
data ACL = ACL { aclVersion :: Int, aclEdges :: [Edge], aclOwners :: [Owner], aclClosed :: [Text], aclPending :: [Pending] }
|
||||
data ACL = ACL {aclVersion :: Int, aclEdges :: [Edge], aclOwners :: [Owner], aclClosed :: [Text], aclPending :: [Pending]}
|
||||
deriving (Eq, Show, Generic)
|
||||
instance ToJSON ACL
|
||||
instance FromJSON ACL where
|
||||
-- 读取权限快照并兼容无声明的旧记录。
|
||||
parseJSON = A.withObject "ACL" $ \fields -> ACL <$> fields A..: "aclVersion" <*> fields A..: "aclEdges"
|
||||
<*> fields A..: "aclOwners" <*> fields A..: "aclClosed" <*> (fields A..:? "aclPending" A..!= [])
|
||||
parseJSON = A.withObject "ACL" $ \fields ->
|
||||
ACL
|
||||
<$> fields A..: "aclVersion"
|
||||
<*> fields A..: "aclEdges"
|
||||
<*> fields A..: "aclOwners"
|
||||
<*> fields A..: "aclClosed"
|
||||
<*> (fields A..:? "aclPending" A..!= [])
|
||||
|
||||
data Context = Context { contextIdentities :: [Account], contextMembers :: [(Text, Text)], contextTargets :: [Target] }
|
||||
data Context = Context {contextIdentities :: [Account], contextMembers :: [(Text, Text)], contextTargets :: [Target]}
|
||||
|
||||
-- 解析存储响应并保留明确错误。
|
||||
request :: Backend -> A.Value -> IO (Either String A.Value)
|
||||
@@ -85,18 +101,20 @@ resolve backend database raw = do
|
||||
| "database:" `T.isPrefixOf` object = (T.drop 9 object, Nothing, "database", object)
|
||||
| object == "*" || ".*" `T.isSuffixOf` object =
|
||||
let scope = if object == "*" then database else T.dropEnd 2 object
|
||||
in (scope, Nothing, "tables", scope <> ".*")
|
||||
in (scope, Nothing, "tables", scope <> ".*")
|
||||
| otherwise = case T.splitOn "." object of
|
||||
[name] -> (database, Just name, "table", database <> "." <> name)
|
||||
[db, name] -> (db, Just name, "table", object)
|
||||
_ -> ("", Just "", "table", object)
|
||||
if T.null selected then pure (Left "no database selected") else do
|
||||
result <- request backend (A.object (["method" A..= ("resolve_object" :: Text), "database" A..= selected] ++ maybe [] (\name -> ["table" A..= name]) table))
|
||||
pure $ do
|
||||
value <- result
|
||||
dbId <- field "database_id" value >>= identifier
|
||||
tableId <- case table of Nothing -> Right Nothing; Just _ -> Just <$> (field "object_id" value >>= identifier)
|
||||
if selected == "system" then Left "system objects cannot be granted" else Right (Target dbId tableId kind label)
|
||||
if T.null selected
|
||||
then pure (Left "no database selected")
|
||||
else do
|
||||
result <- request backend (A.object (["method" A..= ("resolve_object" :: Text), "database" A..= selected] ++ maybe [] (\name -> ["table" A..= name]) table))
|
||||
pure $ do
|
||||
value <- result
|
||||
dbId <- field "database_id" value >>= identifier
|
||||
tableId <- case table of Nothing -> Right Nothing; Just _ -> Just <$> (field "object_id" value >>= identifier)
|
||||
if selected == "system" then Left "system objects cannot be granted" else Right (Target dbId tableId kind label)
|
||||
|
||||
-- 展开当前 catalog 中的稳定业务对象。
|
||||
targets :: Backend -> IO (Either String [Target])
|
||||
@@ -152,28 +170,33 @@ knownRole env value = value `elem` ["0", "public"] || any (\account -> T.pack (s
|
||||
roles :: Context -> Text -> [Text]
|
||||
roles env value = nub ("public" : go [value])
|
||||
where
|
||||
pairs = [(parent, child) |
|
||||
(parentName, childName) <- contextMembers env,
|
||||
Right parent <- [roleId env parentName],
|
||||
active env parent,
|
||||
Right child <- [roleId env childName],
|
||||
active env child]
|
||||
pairs =
|
||||
[ (parent, child)
|
||||
| (parentName, childName) <- contextMembers env
|
||||
, Right parent <- [roleId env parentName]
|
||||
, active env parent
|
||||
, Right child <- [roleId env childName]
|
||||
, active env child
|
||||
]
|
||||
-- 展开成员关系的向上闭包。
|
||||
go known = let more = [parent | (parent, child) <- pairs, child `elem` known, parent `notElem` known]
|
||||
in if null more then known else go (nub (known ++ more))
|
||||
go known =
|
||||
let more = [parent | (parent, child) <- pairs, child `elem` known, parent `notElem` known]
|
||||
in if null more then known else go (nub (known ++ more))
|
||||
|
||||
-- 判断一个对象范围是否覆盖目标。
|
||||
covers :: Target -> Target -> Bool
|
||||
covers source target = targetDatabase source == targetDatabase target &&
|
||||
(targetKind source == "tables" && targetKind target `elem` ["tables", "table"] || targetKind source == targetKind target && targetTable source == targetTable target)
|
||||
covers source target =
|
||||
targetDatabase source == targetDatabase target
|
||||
&& (targetKind source == "tables" && targetKind target `elem` ["tables", "table"] || targetKind source == targetKind target && targetTable source == targetTable target)
|
||||
|
||||
-- 判断稳定对象当前是否存在。
|
||||
exists :: Context -> Target -> Bool
|
||||
exists env target = any same (contextTargets env)
|
||||
where
|
||||
-- 核对数据库或表的稳定编号。
|
||||
same current = targetDatabase current == targetDatabase target &&
|
||||
(targetKind target == "tables" && targetKind current == "database" || targetKind current == targetKind target && targetTable current == targetTable target)
|
||||
same current =
|
||||
targetDatabase current == targetDatabase target
|
||||
&& (targetKind target == "tables" && targetKind current == "database" || targetKind current == targetKind target && targetTable current == targetTable target)
|
||||
|
||||
-- 判断身份是否拥有指定对象。
|
||||
owns :: Context -> ACL -> Text -> Target -> Bool
|
||||
@@ -181,14 +204,15 @@ owns env acl identity target = any (\owner -> ownerRole owner `elem` roles env i
|
||||
|
||||
-- 计算仍有合法授予来源的授权闭包。
|
||||
effective :: Context -> ACL -> ACL
|
||||
effective env acl = acl {aclOwners = owners, aclEdges = grow roots, aclClosed = [value | value <- aclClosed acl, any (\target -> targetKind target == "database" && targetDatabase target == value) (contextTargets env)]}
|
||||
effective env acl = acl{aclOwners = owners, aclEdges = grow roots, aclClosed = [value | value <- aclClosed acl, any (\target -> targetKind target == "database" && targetDatabase target == value) (contextTargets env)]}
|
||||
where
|
||||
owners = [owner | owner <- aclOwners acl, knownRole env (ownerRole owner), exists env (ownerTarget owner)]
|
||||
candidates = [edge | edge <- aclEdges acl, knownRole env (edgeRole edge), knownRole env (edgeGrantor edge), exists env (edgeTarget edge)]
|
||||
roots = [edge | edge <- candidates, edgeRoot edge]
|
||||
-- 从独立授予根展开转授权链。
|
||||
grow known = let more = [edge | edge <- candidates, edge `notElem` known, supported known edge]
|
||||
in if null more then known else grow (known ++ more)
|
||||
grow known =
|
||||
let more = [edge | edge <- candidates, edge `notElem` known, supported known edge]
|
||||
in if null more then known else grow (known ++ more)
|
||||
-- 检查授予者的所有权或有效转授权。
|
||||
supported known edge = active env (edgeGrantor edge) && (owns env acl{aclOwners = owners} (edgeGrantor edge) (edgeTarget edge) || any (\source -> active env (edgeRole source) && edgeOption source && edgeRole source `elem` roles env (edgeGrantor edge) && edgePrivilege source == edgePrivilege edge && covers (edgeTarget source) (edgeTarget edge)) known)
|
||||
|
||||
@@ -201,16 +225,16 @@ readACL backend = do
|
||||
rows <- field "rows" value
|
||||
case rows of
|
||||
A.Array values -> case foldr (:) [] values of
|
||||
[] -> Right (Nothing, Nothing)
|
||||
[row] -> do
|
||||
payload <- field "payload" row
|
||||
case payload of
|
||||
A.String text -> case A.eitherDecodeStrict (TE.encodeUtf8 text) of
|
||||
Left err -> Left ("invalid object ACL: " ++ err)
|
||||
Right acl | aclVersion acl == 1 -> Right (Just text, Just acl)
|
||||
Right _ -> Left "unsupported object ACL version"
|
||||
_ -> Left "invalid object ACL payload"
|
||||
_ -> Left "invalid object ACL rows"
|
||||
[] -> Right (Nothing, Nothing)
|
||||
[row] -> do
|
||||
payload <- field "payload" row
|
||||
case payload of
|
||||
A.String text -> case A.eitherDecodeStrict (TE.encodeUtf8 text) of
|
||||
Left err -> Left ("invalid object ACL: " ++ err)
|
||||
Right acl | aclVersion acl == 1 -> Right (Just text, Just acl)
|
||||
Right _ -> Left "unsupported object ACL version"
|
||||
_ -> Left "invalid object ACL payload"
|
||||
_ -> Left "invalid object ACL rows"
|
||||
_ -> Left "invalid object ACL rows"
|
||||
|
||||
-- 原子写入权限快照并检查并发版本。
|
||||
@@ -230,8 +254,10 @@ migrate backend catalog env = do
|
||||
one options grant = case roleId env (grantRole grant) of
|
||||
Left _ -> pure (Right [])
|
||||
Right identity -> do
|
||||
objects <- if grantObject grant == "*" then pure (Right [target | target <- contextTargets env, targetKind target == "table"])
|
||||
else fmap (fmap (: [])) (resolve backend "" (grantObject grant))
|
||||
objects <-
|
||||
if grantObject grant == "*"
|
||||
then pure (Right [target | target <- contextTargets env, targetKind target == "table"])
|
||||
else fmap (fmap (: [])) (resolve backend "" (grantObject grant))
|
||||
let option = any (\row -> grantRole row == grantRole grant && grantPrivilege row == grantPrivilege grant && grantObject row == grantObject grant) options
|
||||
pure $ case objects of
|
||||
Left "unknown table" -> Right []
|
||||
@@ -248,8 +274,14 @@ load backend catalog = do
|
||||
Left err -> pure (Left err)
|
||||
Right (current, (previous, Just acl)) | null (aclPending acl) -> pure (Right (current, previous, effective current acl))
|
||||
Right (current, (previous, Just acl)) -> do
|
||||
let recovered = [Owner (pendingRole pending) target | pending <- aclPending acl, target <- contextTargets current,
|
||||
targetKind target == "table", targetDatabase target == pendingDatabase pending, targetName target == pendingName pending]
|
||||
let recovered =
|
||||
[ Owner (pendingRole pending) target
|
||||
| pending <- aclPending acl
|
||||
, target <- contextTargets current
|
||||
, targetKind target == "table"
|
||||
, targetDatabase target == pendingDatabase pending
|
||||
, targetName target == pendingName pending
|
||||
]
|
||||
next = acl{aclPending = [], aclOwners = recovered ++ [owner | owner <- aclOwners acl, ownerTarget owner `notElem` map ownerTarget recovered]}
|
||||
written <- writeACL backend previous (effective current next)
|
||||
case written of Left err -> pure (Left err); Right () -> load backend catalog
|
||||
@@ -266,9 +298,10 @@ load backend catalog = do
|
||||
|
||||
-- 检查作用域权限或所有权。
|
||||
allowed :: Context -> ACL -> Text -> Target -> Text -> Bool -> Bool
|
||||
allowed env acl identity target privilege option = owns env acl identity target ||
|
||||
(not option && privilege == "connect" && targetKind target == "database" && targetDatabase target `notElem` aclClosed acl) ||
|
||||
any (\edge -> active env (edgeRole edge) && edgeRole edge `elem` roles env identity && edgePrivilege edge == privilege && covers (edgeTarget edge) target && (not option || edgeOption edge)) (aclEdges acl)
|
||||
allowed env acl identity target privilege option =
|
||||
owns env acl identity target
|
||||
|| (not option && privilege == "connect" && targetKind target == "database" && targetDatabase target `notElem` aclClosed acl)
|
||||
|| any (\edge -> active env (edgeRole edge) && edgeRole edge `elem` roles env identity && edgePrivilege edge == privilege && covers (edgeTarget edge) target && (not option || edgeOption edge)) (aclEdges acl)
|
||||
|
||||
-- 按对象类型展开和校验权限名称。
|
||||
privileges :: Target -> [Text] -> Either String [Text]
|
||||
@@ -276,10 +309,14 @@ privileges target names =
|
||||
if all (`elem` supported) expanded then Right expanded else Left "invalid privilege for object scope"
|
||||
where
|
||||
supported = if targetKind target == "database" then ["connect", "create"] else ["select", "insert", "update", "delete"]
|
||||
expanded = nub (concatMap
|
||||
((\ name -> if name == "all" then supported else [name])
|
||||
. T.toLower)
|
||||
names)
|
||||
expanded =
|
||||
nub
|
||||
( concatMap
|
||||
( (\name -> if name == "all" then supported else [name])
|
||||
. T.toLower
|
||||
)
|
||||
names
|
||||
)
|
||||
|
||||
-- 检查一组稳定对象上的业务权限。
|
||||
objectAuthorize :: Backend -> Catalog -> Principal -> Text -> [(Text, Text)] -> IO (Either String ())
|
||||
@@ -310,18 +347,19 @@ objectGrant backend catalog principal database names object role option = do
|
||||
recipient <- roleId env role
|
||||
items <- privileges target names
|
||||
grantor <- if principalIsRoot principal then Right (fromRight "0" (roleId env (principalName principal))) else roleId env (principalName principal)
|
||||
case [item |
|
||||
not (principalIsRoot principal),
|
||||
item <- items,
|
||||
not (allowed env acl grantor target item True)] of
|
||||
case [ item
|
||||
| not (principalIsRoot principal)
|
||||
, item <- items
|
||||
, not (allowed env acl grantor target item True)
|
||||
] of
|
||||
item : _ -> Left ("grant option required: " ++ T.unpack (T.toUpper item <> " ON " <> object))
|
||||
[] -> Right (recipient, grantor, items) of
|
||||
Left err -> pure (Left err)
|
||||
Right (recipient, grantor, items) -> do
|
||||
let edges = [Edge recipient grantor target item option (principalIsRoot principal) | item <- items]
|
||||
remaining = [edge | edge <- aclEdges acl, not (any (same edge) edges)]
|
||||
promoted = [edge {edgeOption = edgeOption edge || any (\old -> same old edge && edgeOption old) (aclEdges acl)} | edge <- edges]
|
||||
writeACL backend previous acl{aclEdges = remaining ++ promoted, aclClosed = if recipient == "public" && "connect" `elem` items then filter (/= targetDatabase target) (aclClosed acl) else aclClosed acl}
|
||||
Left err -> pure (Left err)
|
||||
Right (recipient, grantor, items) -> do
|
||||
let edges = [Edge recipient grantor target item option (principalIsRoot principal) | item <- items]
|
||||
remaining = [edge | edge <- aclEdges acl, not (any (same edge) edges)]
|
||||
promoted = [edge{edgeOption = edgeOption edge || any (\old -> same old edge && edgeOption old) (aclEdges acl)} | edge <- edges]
|
||||
writeACL backend previous acl{aclEdges = remaining ++ promoted, aclClosed = if recipient == "public" && "connect" `elem` items then filter (/= targetDatabase target) (aclClosed acl) else aclClosed acl}
|
||||
where
|
||||
-- 比较同一授予来源的授权键。
|
||||
same one other = edgeRole one == edgeRole other && edgeGrantor one == edgeGrantor other && edgeTarget one == edgeTarget other && edgePrivilege one == edgePrivilege other
|
||||
@@ -350,7 +388,8 @@ revokeObject optionOnly backend catalog principal database names object role = d
|
||||
next = acl{aclEdges = if optionOnly then map removeOption (aclEdges acl) else filter (not . matches) (aclEdges acl), aclClosed = if not optionOnly && recipient == "public" && "connect" `elem` items then nub (targetDatabase target : aclClosed acl) else aclClosed acl}
|
||||
-- 保留基础权限并移除匹配来源的转授权。
|
||||
removeOption edge = if matches edge then edge{edgeOption = False} else edge
|
||||
if recipient == "public" && "connect" `elem` items && not manager then pure (Left "administrator required")
|
||||
if recipient == "public" && "connect" `elem` items && not manager
|
||||
then pure (Left "administrator required")
|
||||
else writeACL backend previous (effective env next)
|
||||
|
||||
-- 返回公开名称形式的有效授权。
|
||||
@@ -373,10 +412,12 @@ objectSelfViews backend catalog principal = do
|
||||
if active env identity then Right () else Left "permission denied: disabled identity"
|
||||
-- 使用当前权限闭包判断普通及转授权权限。
|
||||
let permitted target privilege option = principalIsRoot principal || allowed env acl identity target privilege option
|
||||
Right [Grant (principalName principal) privilege (targetName target) (permitted target privilege True)
|
||||
Right
|
||||
[ Grant (principalName principal) privilege (targetName target) (permitted target privilege True)
|
||||
| target <- contextTargets env
|
||||
, privilege <- if targetKind target == "database" then ["connect", "create"] else ["select", "insert", "update", "delete"]
|
||||
, permitted target privilege False]
|
||||
, permitted target privilege False
|
||||
]
|
||||
|
||||
-- 登记新对象的创建者为所有者。
|
||||
objectClaim :: Backend -> Catalog -> Principal -> Text -> Text -> IO (Either String ())
|
||||
@@ -400,7 +441,8 @@ objectPrepareCreate backend catalog principal database raw = do
|
||||
Left err -> pure (Left err)
|
||||
Right identity -> do
|
||||
let name = if "." `T.isInfixOf` raw then T.toLower raw else database <> "." <> T.toLower raw
|
||||
if any ((== name) . targetName) (contextTargets env) then pure (Right ())
|
||||
if any ((== name) . targetName) (contextTargets env)
|
||||
then pure (Right ())
|
||||
else writeACL backend previous acl{aclPending = [Pending identity (targetDatabase target) name]}
|
||||
|
||||
-- 清理失效身份、成员来源和已删除对象。
|
||||
@@ -417,8 +459,9 @@ objectAffected backend catalog names = do
|
||||
(env, _, acl) <- loaded
|
||||
let edges = [(roleName env (edgeGrantor edge), roleName env (edgeRole edge)) | edge <- aclEdges acl] ++ contextMembers env
|
||||
-- 展开成员和转授权的受影响身份。
|
||||
walk seen = let more = [child | (parent, child) <- edges, parent `elem` seen, child `notElem` seen, not (T.null child)]
|
||||
in if null more then seen else walk (nub (seen ++ more))
|
||||
walk seen =
|
||||
let more = [child | (parent, child) <- edges, parent `elem` seen, child `notElem` seen, not (T.null child)]
|
||||
in if null more then seen else walk (nub (seen ++ more))
|
||||
let normalized = map (T.toLower . T.strip) names
|
||||
Right (if "public" `elem` normalized then nub (walk normalized ++ map accountUser (filter (not . accountIsSuperuser) (contextIdentities env))) else walk normalized)
|
||||
|
||||
@@ -441,7 +484,8 @@ objectUsers backend catalog database object = do
|
||||
case (,) <$> loaded <*> resolved of
|
||||
Left err -> pure (Left err)
|
||||
Right ((env, _, acl), target) -> do
|
||||
let related = [edgeRole edge | edge <- aclEdges acl, if targetKind target == "database" then targetDatabase (edgeTarget edge) == targetDatabase target else covers (edgeTarget edge) target]
|
||||
++ [ownerRole owner | owner <- aclOwners acl, ownerTarget owner == target]
|
||||
let related =
|
||||
[edgeRole edge | edge <- aclEdges acl, if targetKind target == "database" then targetDatabase (edgeTarget edge) == targetDatabase target else covers (edgeTarget edge) target]
|
||||
++ [ownerRole owner | owner <- aclOwners acl, ownerTarget owner == target]
|
||||
result <- objectAffected backend catalog (nub (map (roleName env) related))
|
||||
pure (fmap (filter (\name -> not (any (\account -> accountUser account == name && accountIsSuperuser account) (contextIdentities env)))) result)
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
module ChuSQL.Server.Policy (
|
||||
configurePasswordPolicy,
|
||||
policyFromValues,
|
||||
|
||||
@@ -1,31 +1,55 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module ChuSQL.Server.Privileges
|
||||
( Grant (..), Membership (..), PrivilegeCommand (..), PrivilegeError (..), Privileges, RoleView (..)
|
||||
, affectedAccounts, authorize, authorizeTables, authorizeConnect, filterTables, listRoleViews, listSelfGrants
|
||||
, newPrivileges, normalizeObject, privilegeCommand, runPrivilegeCommand, claimObject, prepareObject, reconcileObjects, affectedObject
|
||||
) where
|
||||
module ChuSQL.Server.Privileges (
|
||||
Grant (..),
|
||||
Membership (..),
|
||||
PrivilegeCommand (..),
|
||||
PrivilegeError (..),
|
||||
Privileges,
|
||||
RoleView (..),
|
||||
affectedAccounts,
|
||||
authorize,
|
||||
authorizeTables,
|
||||
authorizeConnect,
|
||||
filterTables,
|
||||
listRoleViews,
|
||||
listSelfGrants,
|
||||
newPrivileges,
|
||||
normalizeObject,
|
||||
privilegeCommand,
|
||||
runPrivilegeCommand,
|
||||
claimObject,
|
||||
prepareObject,
|
||||
reconcileObjects,
|
||||
affectedObject,
|
||||
) where
|
||||
|
||||
import Data.Maybe (maybeToList)
|
||||
import ChuSQL.Core.Engine.Syntax.AST
|
||||
import ChuSQL.Core.Protocol (Account (..))
|
||||
import ChuSQL.Interface.Protocol (Grant (..), RoleView (..))
|
||||
import ChuSQL.Server.Accounts (Principal, principalIsRoot, principalIsCatalogManager)
|
||||
import ChuSQL.Server.Accounts (Principal, principalIsCatalogManager, principalIsRoot)
|
||||
import ChuSQL.Server.Backend (Backend (..), exprTables, statementTables, systemDatabaseName)
|
||||
import ChuSQL.Server.Catalog
|
||||
import qualified ChuSQL.Server.ObjectACL as OA
|
||||
import Data.List (nub, sort)
|
||||
import Data.Maybe (maybeToList)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
|
||||
-- 权限服务:稳定对象授权、角色继承与受影响会话展开。
|
||||
|
||||
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 Bool | RevokePrivilegesCommand [Text] Text Text | RevokeGrantOptionCommand [Text] Text Text
|
||||
| GrantRoleCommand Text [Text] | RevokeRoleCommand Text [Text] deriving (Show, Eq)
|
||||
data Privileges = Privileges { pvCatalog :: Catalog, pvBackend :: Backend }
|
||||
data Membership = Membership {memberRole :: Text, memberUser :: Text} deriving (Show, Eq, Ord)
|
||||
data PrivilegeCommand
|
||||
= CreateRoleCommand Text
|
||||
| DropRoleCommand Text
|
||||
| GrantPrivilegesCommand [Text] Text Text Bool
|
||||
| RevokePrivilegesCommand [Text] Text Text
|
||||
| RevokeGrantOptionCommand [Text] Text Text
|
||||
| GrantRoleCommand Text [Text]
|
||||
| RevokeRoleCommand Text [Text]
|
||||
deriving (Show, Eq)
|
||||
data Privileges = Privileges {pvCatalog :: Catalog, pvBackend :: Backend}
|
||||
|
||||
-- 创建权限目录服务。
|
||||
newPrivileges :: Backend -> IO Privileges
|
||||
@@ -56,9 +80,10 @@ runPrivilegeCommand service principal database command = withTables service $ ca
|
||||
known <- catalog (readRoleNames directory)
|
||||
case known of
|
||||
Left err -> pure (Left err)
|
||||
Right names | normalizeRole role `elem` names -> pure (Left (PrivilegeError "conflict" ("role already exists: " <> role)))
|
||||
| normalizeRole role == "public" -> pure (Left (PrivilegeError "bad_request" "public is reserved for default privileges"))
|
||||
| otherwise -> catalog (insertRole directory role)
|
||||
Right names
|
||||
| normalizeRole role `elem` names -> pure (Left (PrivilegeError "conflict" ("role already exists: " <> role)))
|
||||
| normalizeRole role == "public" -> pure (Left (PrivilegeError "bad_request" "public is reserved for default privileges"))
|
||||
| otherwise -> catalog (insertRole directory role)
|
||||
DropRoleCommand role
|
||||
| principalIsRoot principal -> after (catalog (dropRole directory role))
|
||||
GrantRoleCommand role members
|
||||
@@ -94,12 +119,14 @@ validateMembers names pairs role members
|
||||
foldMembers edges (member : rest)
|
||||
| child `elem` closureUp edges [parent] = Left (PrivilegeError "conflict" ("role membership would create a cycle: " <> parent <> " and " <> child))
|
||||
| otherwise = foldMembers ((parent, child) : edges) rest
|
||||
where child = normalizeUser member
|
||||
where
|
||||
child = normalizeUser member
|
||||
|
||||
-- 展开角色的继承闭包。
|
||||
closureUp :: [(Text, Text)] -> [Text] -> [Text]
|
||||
closureUp edges known = let more = [parent | (parent, child) <- edges, child `elem` known, parent `notElem` known]
|
||||
in if null more then known else closureUp edges (nub (known ++ more))
|
||||
closureUp edges known =
|
||||
let more = [parent | (parent, child) <- edges, child `elem` known, parent `notElem` known]
|
||||
in if null more then known else closureUp edges (nub (known ++ more))
|
||||
|
||||
-- 展开成员和转授权变更影响的全部身份。
|
||||
affectedAccounts :: Privileges -> PrivilegeCommand -> IO (Either PrivilegeError [Text])
|
||||
@@ -173,12 +200,13 @@ listRoleViews service = withTables service $ do
|
||||
members <- catalog (readMembers (pvCatalog service))
|
||||
case (,) <$> names <*> members of
|
||||
Left err -> pure (Left err)
|
||||
Right (identities, pairs) -> sequence
|
||||
<$>
|
||||
mapM
|
||||
(view pairs)
|
||||
(sort
|
||||
(map accountUser (filter (not . accountCanLogin) identities)))
|
||||
Right (identities, pairs) ->
|
||||
sequence
|
||||
<$> mapM
|
||||
(view pairs)
|
||||
( sort
|
||||
(map accountUser (filter (not . accountCanLogin) identities))
|
||||
)
|
||||
where
|
||||
-- 组装一个角色的公开权限视图。
|
||||
view members name = do
|
||||
@@ -228,13 +256,14 @@ catalog = fmap (either (Left . storageError) Right)
|
||||
storageError :: String -> PrivilegeError
|
||||
storageError message = PrivilegeError code (T.pack message)
|
||||
where
|
||||
code | message == "no database selected" = "no_database"
|
||||
| any (`T.isPrefixOf` T.pack message) ["permission denied:", "grant option required:", "object owner required", "administrator required", "system objects cannot be granted"] = "forbidden"
|
||||
| any (`T.isPrefixOf` T.pack message) ["unknown role:", "unknown table", "unknown database"] = "not_found"
|
||||
| message == "object ACL changed concurrently" = "conflict"
|
||||
| message == "invalid privilege for object scope" = "bad_request"
|
||||
| message == "the last enabled login superuser cannot be removed" = "bad_request"
|
||||
| otherwise = "storage_error"
|
||||
code
|
||||
| message == "no database selected" = "no_database"
|
||||
| any (`T.isPrefixOf` T.pack message) ["permission denied:", "grant option required:", "object owner required", "administrator required", "system objects cannot be granted"] = "forbidden"
|
||||
| any (`T.isPrefixOf` T.pack message) ["unknown role:", "unknown table", "unknown database"] = "not_found"
|
||||
| message == "object ACL changed concurrently" = "conflict"
|
||||
| message == "invalid privilege for object scope" = "bad_request"
|
||||
| message == "the last enabled login superuser cannot be removed" = "bad_request"
|
||||
| otherwise = "storage_error"
|
||||
|
||||
forbidden :: PrivilegeError
|
||||
forbidden = PrivilegeError "forbidden" "administrator required"
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
module ChuSQL.Server.Security (readSecurity, writeSecurity) where
|
||||
module ChuSQL.Server.Security (readSecurity, writeSecurity, databaseSecurity, commitSecurity) where
|
||||
|
||||
import Control.Monad (when)
|
||||
import Control.Concurrent.MVar
|
||||
import Control.Exception (bracket_, mask_)
|
||||
import Control.Exception (bracket, bracket_, mask_)
|
||||
import Control.Monad (when)
|
||||
import qualified Data.Map.Strict as Map
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
|
||||
-- 共享权限门:查询并行读取,权限和对象变更独占执行。
|
||||
-- 权限变更独占全局门,业务按库排序取门,允许不同库并行写。
|
||||
|
||||
data Gate = Gate (MVar ()) (MVar ()) (MVar Int)
|
||||
|
||||
-- 创建公平的共享读写门。
|
||||
newGate :: IO Gate
|
||||
newGate = Gate <$> newMVar () <*> newMVar () <*> newMVar 0
|
||||
|
||||
{-# NOINLINE securityGate #-}
|
||||
securityGate :: (MVar (), MVar (), MVar Int)
|
||||
securityGate = unsafePerformIO $ do
|
||||
turn <- newMVar ()
|
||||
room <- newMVar ()
|
||||
readers <- newMVar 0
|
||||
pure (turn, room, readers)
|
||||
securityGate :: Gate
|
||||
securityGate = unsafePerformIO newGate
|
||||
|
||||
-- 并行执行权限稳定期间的查询。
|
||||
readSecurity :: IO a -> IO a
|
||||
readSecurity = bracket_ enter leave
|
||||
{-# NOINLINE databaseGates #-}
|
||||
databaseGates :: MVar (Map.Map String (Gate, Int))
|
||||
databaseGates = unsafePerformIO (newMVar Map.empty)
|
||||
|
||||
-- 在共享门内执行并保证取消时归还计数。
|
||||
readGate :: Gate -> IO a -> IO a
|
||||
readGate (Gate turn room readers) = bracket_ enter leave
|
||||
where
|
||||
(turn, room, readers) = securityGate
|
||||
-- 登记共享读取者。
|
||||
enter = withMVar turn $ \_ -> modifyMVar_ readers $ \count -> do
|
||||
when (count == 0) $ takeMVar room
|
||||
@@ -29,8 +35,46 @@ readSecurity = bracket_ enter leave
|
||||
when (count == 1) $ putMVar room ()
|
||||
pure (count - 1)
|
||||
|
||||
-- 独占执行权限或对象变更。
|
||||
-- 独占执行写入及提交的校验与发布。
|
||||
writeGate :: Gate -> IO a -> IO a
|
||||
writeGate (Gate turn room _) action = withMVar turn $ \_ -> withMVar room (const action)
|
||||
|
||||
-- 并行执行权限稳定期间的查询。
|
||||
readSecurity :: IO a -> IO a
|
||||
readSecurity = readGate securityGate
|
||||
|
||||
-- 全局排除权限与对象变更期间的数据请求。
|
||||
writeSecurity :: IO a -> IO a
|
||||
writeSecurity action = withMVar turn $ \_ -> withMVar room (const action)
|
||||
writeSecurity = writeGate securityGate
|
||||
|
||||
-- 按名称取库门,固定顺序避免跨库等待环。
|
||||
databaseSecurity :: [(String, Bool)] -> IO a -> IO a
|
||||
databaseSecurity accesses action = readSecurity (keyedSecurity accesses action)
|
||||
|
||||
-- 只排序短暂提交检查,不阻塞行锁等待与准备。
|
||||
commitSecurity :: String -> IO a -> IO a
|
||||
commitSecurity database = keyedSecurity [("\0commit:" ++ database, True)]
|
||||
|
||||
-- 管理有序键门及引用生命周期。
|
||||
keyedSecurity :: [(String, Bool)] -> IO a -> IO a
|
||||
keyedSecurity accesses action = bracket register unregister $ \gates -> lock gates action
|
||||
where
|
||||
(turn, room, _) = securityGate
|
||||
names = Map.toAscList (Map.fromListWith (||) (filter (not . null . fst) accesses))
|
||||
-- 先登记所有等待者,防止等待中的门被释放。
|
||||
register = modifyMVar databaseGates $ \existing -> do
|
||||
entries <- mapM (entry existing) names
|
||||
let updated = foldr (\(name, gate, count, _) -> Map.insert name (gate, count)) existing entries
|
||||
pure (updated, entries)
|
||||
-- 复用活跃门或创建首次访问的门。
|
||||
entry existing (name, exclusive) = case Map.lookup name existing of
|
||||
Just (gate, count) -> pure (name, gate, count + 1, exclusive)
|
||||
Nothing -> do
|
||||
gate <- newGate
|
||||
pure (name, gate, 1, exclusive)
|
||||
-- 最后一个持有者或等待者离开时回收门。
|
||||
unregister entries = modifyMVar_ databaseGates $ \existing -> pure (foldr release existing entries)
|
||||
-- 使用当前引用计数,避免覆盖并发登记。
|
||||
release (name, _, _, _) = Map.update (\(gate, count) -> if count == 1 then Nothing else Just (gate, count - 1)) name
|
||||
-- 递归持有有序门并在异常时逆序释放。
|
||||
lock [] body = body
|
||||
lock ((_, gate, _, exclusive) : rest) body = (if exclusive then writeGate else readGate) gate (lock rest body)
|
||||
@@ -1,68 +1,73 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module ChuSQL.Server.Session
|
||||
( QueryResult (..)
|
||||
, Session
|
||||
, SessionError (..)
|
||||
, engineErrorCode
|
||||
, newSession
|
||||
, newSessionWith
|
||||
, sessionUser
|
||||
, sessionIsAdmin
|
||||
, sessionDatabase
|
||||
, isPlainIdentifier
|
||||
, authenticateSession
|
||||
, authenticateSessionCoded
|
||||
, authenticateSudoSessionCoded
|
||||
, switchDatabase
|
||||
, runStatement
|
||||
, runStatementCoded
|
||||
, runStorageCoded
|
||||
, analyzeNormalizationCoded
|
||||
, catalog
|
||||
, accounts
|
||||
, roleViews
|
||||
, databases
|
||||
, policyOf
|
||||
, reloadPolicy
|
||||
, queryResultJson
|
||||
) where
|
||||
module ChuSQL.Server.Session (
|
||||
QueryResult (..),
|
||||
Session,
|
||||
SessionError (..),
|
||||
engineErrorCode,
|
||||
newSession,
|
||||
closeSession,
|
||||
newSessionWith,
|
||||
sessionUser,
|
||||
sessionIsAdmin,
|
||||
sessionDatabase,
|
||||
isPlainIdentifier,
|
||||
authenticateSession,
|
||||
authenticateSessionCoded,
|
||||
authenticateSudoSessionCoded,
|
||||
switchDatabase,
|
||||
runStatement,
|
||||
runStatementCoded,
|
||||
runStorageCoded,
|
||||
analyzeNormalizationCoded,
|
||||
catalog,
|
||||
accounts,
|
||||
roleViews,
|
||||
databases,
|
||||
policyOf,
|
||||
reloadPolicy,
|
||||
queryResultJson,
|
||||
) where
|
||||
|
||||
import ChuSQL.Core.Model (Database, Row, Table (..), Value (..), Column (..), typeName)
|
||||
import ChuSQL.Core.Protocol
|
||||
( SchemaColumn(..),
|
||||
Account(..),
|
||||
Request(..),
|
||||
QueryResult(..),
|
||||
TxnOp(..),
|
||||
queryResultJson )
|
||||
import ChuSQL.Interface.Normalization
|
||||
import ChuSQL.Server.Normalization (normalizationBudget, normalizationReport)
|
||||
import Data.Aeson.Types (parseEither)
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import ChuSQL.Core.Engine.Storage.IPC (TableInfo (..))
|
||||
import qualified ChuSQL.Core.Engine as Engine
|
||||
import qualified ChuSQL.Core.Engine.Error as E
|
||||
import ChuSQL.Core.Engine.Runtime.Dependencies (domainDependencies, queryTables, validateDependencies)
|
||||
import ChuSQL.Core.Engine.Storage.IPC (TableInfo (..))
|
||||
import ChuSQL.Core.Engine.Syntax.AST (Statement (..))
|
||||
import ChuSQL.Core.Engine.Syntax.Parser (parseStatement)
|
||||
import ChuSQL.Core.Model (Column (..), Database, Row, Table (..), Value (..), lastTablePart, typeName)
|
||||
import ChuSQL.Core.Protocol (
|
||||
Account (..),
|
||||
QueryResult (..),
|
||||
Request (..),
|
||||
SchemaColumn (..),
|
||||
TxnOp (..),
|
||||
queryResultJson,
|
||||
)
|
||||
import ChuSQL.Interface.AccountTable (systemTableInfo)
|
||||
import ChuSQL.Interface.Auth (defaultSessionPolicy, newSessionStore)
|
||||
import ChuSQL.Interface.Normalization
|
||||
import ChuSQL.Server.Accounts
|
||||
import ChuSQL.Server.Backend (Backend (..), StatementResult (..), columnsFromStatement, scopeDdl)
|
||||
import ChuSQL.Server.Backend (Backend (..), StatementResult (..), applyTransactionMemory, columnsFromStatement, scopeDdl)
|
||||
import ChuSQL.Server.Normalization (normalizationBudget, normalizationReport)
|
||||
import ChuSQL.Server.Policy (configurePasswordPolicy)
|
||||
import ChuSQL.Server.Privileges (PrivilegeCommand (..), PrivilegeError (..), Privileges, RoleView, Grant (..), affectedAccounts, affectedObject, authorize, authorizeTables, authorizeConnect, claimObject, prepareObject, reconcileObjects, filterTables, listRoleViews, listSelfGrants, newPrivileges, privilegeCommand, runPrivilegeCommand)
|
||||
import ChuSQL.Server.Security (readSecurity, writeSecurity)
|
||||
import ChuSQL.Server.Privileges (Grant (..), PrivilegeCommand (..), PrivilegeError (..), Privileges, RoleView, affectedAccounts, affectedObject, authorize, authorizeConnect, authorizeTables, claimObject, filterTables, listRoleViews, listSelfGrants, newPrivileges, prepareObject, privilegeCommand, reconcileObjects, runPrivilegeCommand)
|
||||
import ChuSQL.Server.Security (commitSecurity, databaseSecurity, readSecurity, writeSecurity)
|
||||
import Control.Exception (IOException, try)
|
||||
import Data.Bifunctor (first)
|
||||
import Data.Char (isAlpha, isAlphaNum)
|
||||
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
|
||||
import Data.Maybe (isJust, fromMaybe)
|
||||
import qualified Data.IntMap.Strict as IM
|
||||
import qualified Data.IntSet as IS
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import Data.Aeson.Types (parseEither)
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import Data.Char (isAlpha, isAlphaNum)
|
||||
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
|
||||
import qualified Data.IntMap.Strict as IM
|
||||
import qualified Data.IntSet as IS
|
||||
import Data.List (nub)
|
||||
import Data.Maybe (fromMaybe, isJust)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
|
||||
-- SQL 会话:Web、命令行与 TCP 服务器共用的账号、权限与语句执行入口。
|
||||
@@ -97,6 +102,8 @@ data Transaction = Transaction
|
||||
, txSavepoints :: [(String, Database, [Statement])]
|
||||
, txStatements :: [Statement]
|
||||
, txObjects :: [(String, A.Value)]
|
||||
, txToken :: Maybe String
|
||||
, txSerializable :: Bool
|
||||
}
|
||||
|
||||
-- | 开一个会话:账号与角色服务、配置策略都和 Web 端一致
|
||||
@@ -117,6 +124,18 @@ newSessionWith backend name settingsFile changed = do
|
||||
transaction <- newIORef Nothing
|
||||
pure (Session backend accountsService privileges current principal identityStamp settingsFile transaction changed)
|
||||
|
||||
-- 关闭连接时归还存储事务、版本引用与行锁。
|
||||
closeSession :: Session -> IO ()
|
||||
closeSession session = do
|
||||
existing <- readIORef (ssTransaction session)
|
||||
writeIORef (ssTransaction session) Nothing
|
||||
case existing >>= txToken of
|
||||
Nothing -> pure ()
|
||||
Just token -> do
|
||||
backend <- currentBackend session
|
||||
result <- beRollbackTransaction backend token
|
||||
either (ioError . userError) pure result
|
||||
|
||||
-- | 当前生效的口令策略(只有管理员能看)
|
||||
policyOf :: Session -> IO (Either Text PasswordPolicy)
|
||||
policyOf session = do
|
||||
@@ -195,7 +214,7 @@ storePrincipal session principal = do
|
||||
Right account -> do
|
||||
writeIORef (ssPrincipal session) (Just principal)
|
||||
writeIORef (ssIdentityStamp session) (Just (accountId account, accountRevision account))
|
||||
writeIORef (ssTransaction session) Nothing
|
||||
closeSession session
|
||||
writeIORef (ssCurrent session) ""
|
||||
pure (Right ())
|
||||
|
||||
@@ -216,7 +235,7 @@ switchDatabaseCoded session rawName
|
||||
| otherwise = go
|
||||
where
|
||||
name = T.toLower (T.strip rawName)
|
||||
-- | 真的切库并记下当前库
|
||||
-- \| 真的切库并记下当前库
|
||||
go = do
|
||||
who <- readIORef (ssPrincipal session)
|
||||
allowed <- case who of
|
||||
@@ -237,17 +256,35 @@ runStatement session sql = fmap (first sessMessage) (runStatementCoded session s
|
||||
|
||||
-- | 跑一条语句(带错误码)
|
||||
runStatementCoded :: Session -> Text -> IO (Either SessionError QueryResult)
|
||||
runStatementCoded session sql = gate $ do
|
||||
valid <- validatePrincipal session
|
||||
case valid of Left err -> pure (Left err); Right () -> runStatementUnlocked session sql
|
||||
runStatementCoded session sql = do
|
||||
current <- readIORef (ssCurrent session)
|
||||
gate current $ do
|
||||
valid <- validatePrincipal session
|
||||
case valid of Left err -> pure (Left err); Right () -> runStatementUnlocked session sql
|
||||
where
|
||||
gate = case parseStatement (T.unpack sql) of
|
||||
-- 对象及权限变更独占,数据请求按来源库排序。
|
||||
gate current = case parseStatement (T.unpack sql) of
|
||||
Right CommitTransaction -> databaseSecurity [(T.unpack current, not (beConcurrentTransactions (ssBackend session)))]
|
||||
Right statement | transactionDdl statement || isJust (accountCommand statement) || isJust (privilegeCommand statement) -> writeSecurity
|
||||
_ -> readSecurity
|
||||
Right statement -> databaseSecurity (accesses current statement)
|
||||
Left _ -> readSecurity
|
||||
-- 当前库写入独占,跨库查询依赖共享。
|
||||
accesses current statement =
|
||||
(T.unpack current, writing statement && not (beConcurrentTransactions (ssBackend session))) : [(source name, False) | name <- queryTables statement]
|
||||
where
|
||||
-- 无限定表使用当前工作库。
|
||||
source name = case break (== '.') name of
|
||||
(database, '.' : _) -> T.unpack (T.toLower (T.pack database))
|
||||
_ -> T.unpack current
|
||||
-- 只有数据写语句需要独占目标库。
|
||||
writing Insert{} = True
|
||||
writing Update{} = True
|
||||
writing Delete{} = True
|
||||
writing _ = False
|
||||
|
||||
-- 在权限门内检查并执行管理员存储请求。
|
||||
runStorageCoded :: Session -> A.Value -> IO (Either SessionError A.Value)
|
||||
runStorageCoded session payload = writeSecurity $ do
|
||||
runStorageCoded session payload = gate $ do
|
||||
valid <- validatePrincipal session
|
||||
who <- readIORef (ssPrincipal session)
|
||||
case valid of
|
||||
@@ -256,6 +293,11 @@ runStorageCoded session payload = writeSecurity $ do
|
||||
Nothing -> pure (Left (SessionError "unauthorized" "sign in first"))
|
||||
Just principal | not (principalIsRoot principal) -> pure (Left (SessionError "forbidden" "administrator required"))
|
||||
Just _ -> fmap (first (SessionError "storage_error" . T.pack)) (beStorage (ssBackend session) payload)
|
||||
where
|
||||
-- 在线整理期间允许普通数据语句进入。
|
||||
gate = case payload of
|
||||
A.Object object | KM.lookup "method" object == Just (A.String "compact") -> readSecurity
|
||||
_ -> writeSecurity
|
||||
|
||||
-- 检查登录身份的当前版本和启用状态。
|
||||
validatePrincipal :: Session -> IO (Either SessionError ())
|
||||
@@ -285,7 +327,8 @@ runStatementUnlocked session sql = case parseStatement (T.unpack sql) of
|
||||
Right () -> do
|
||||
current <- readIORef (ssCurrent session)
|
||||
pure (Right (emptyResult (Just current)))
|
||||
Right BeginTransaction -> beginTransaction session
|
||||
Right BeginTransaction -> beginTransaction session False
|
||||
Right BeginSerializableTransaction -> beginTransaction session True
|
||||
Right CommitTransaction -> commitTransaction session
|
||||
Right RollbackTransaction -> rollbackTransaction session
|
||||
Right (Savepoint name) -> savepointTransaction session (T.pack name)
|
||||
@@ -306,6 +349,7 @@ runStatementUnlocked session sql = case parseStatement (T.unpack sql) of
|
||||
Left err -> pure (Left err)
|
||||
Right ()
|
||||
| isJust transaction && transactionData statement -> runInTransaction session statement
|
||||
| beConcurrentTransactions (ssBackend session) && transactionData statement -> runAutocommit session statement
|
||||
| otherwise -> runObjectStatement session statement sql
|
||||
Left _ -> runPlain session sql
|
||||
|
||||
@@ -314,6 +358,7 @@ transactionDdl :: Statement -> Bool
|
||||
transactionDdl statement = case statement of
|
||||
CreateTable{} -> True
|
||||
CreateDomain{} -> True
|
||||
AlterDomain{} -> True
|
||||
DropDomain{} -> True
|
||||
DropTable{} -> True
|
||||
CreateIndex{} -> True
|
||||
@@ -339,14 +384,14 @@ transactionData statement = case statement of
|
||||
_ -> False
|
||||
|
||||
-- | 开事务:把当前库整库做一份快照当起点
|
||||
beginTransaction :: Session -> IO (Either SessionError QueryResult)
|
||||
beginTransaction session = do
|
||||
beginTransaction :: Session -> Bool -> IO (Either SessionError QueryResult)
|
||||
beginTransaction session serializable = do
|
||||
allowed <- sessionConnectAllowed session
|
||||
case allowed of Left err -> pure (Left err); Right () -> beginAuthorized session
|
||||
case allowed of Left err -> pure (Left err); Right () -> beginAuthorized session serializable
|
||||
|
||||
-- 在连接权限复核后建立事务快照。
|
||||
beginAuthorized :: Session -> IO (Either SessionError QueryResult)
|
||||
beginAuthorized session = do
|
||||
beginAuthorized :: Session -> Bool -> IO (Either SessionError QueryResult)
|
||||
beginAuthorized session serializable = do
|
||||
current <- readIORef (ssCurrent session)
|
||||
existing <- readIORef (ssTransaction session)
|
||||
case existing of
|
||||
@@ -355,15 +400,20 @@ beginAuthorized session = do
|
||||
| T.null current -> pure (Left (SessionError "no_database" "no database selected"))
|
||||
| otherwise -> do
|
||||
backend <- currentBackend session
|
||||
snapshotResult <- beSnapshot backend
|
||||
snapshotResult <-
|
||||
if beConcurrentTransactions backend
|
||||
then beBeginTransaction backend serializable
|
||||
else fmap (fmap (Nothing,)) (beSnapshot backend)
|
||||
case snapshotResult of
|
||||
Left err -> pure (Left (engineErrorCode err))
|
||||
Right db -> do
|
||||
Right (token, db) -> do
|
||||
objects <- snapshotObjectIds session (map fst db)
|
||||
case objects of
|
||||
Left err -> pure (Left err)
|
||||
Left err -> do
|
||||
released <- maybe (pure (Right ())) (beRollbackTransaction backend) token
|
||||
pure (either (Left . engineErrorCode) (const (Left err)) released)
|
||||
Right ids -> do
|
||||
writeIORef (ssTransaction session) (Just (Transaction db db [] [] ids))
|
||||
writeIORef (ssTransaction session) (Just (Transaction db db [] [] ids token serializable))
|
||||
pure (Right (emptyResult (Just current)))
|
||||
|
||||
-- | 提交:把快照差分交给存储层应用,成功才结束事务
|
||||
@@ -379,25 +429,84 @@ commitTransaction session = do
|
||||
identities <- snapshotObjectIds session (map fst (txBase transaction))
|
||||
case connected >> sequence permissions >> identities of
|
||||
Left err -> pure (Left err)
|
||||
Right ids | ids /= txObjects transaction -> pure (Left (SessionError "serialization_failure" "transaction object identity changed"))
|
||||
| otherwise -> commitAuthorized session current transaction
|
||||
Right ids
|
||||
| ids /= txObjects transaction -> pure (Left (SessionError "serialization_failure" "transaction object identity changed"))
|
||||
| otherwise -> commitAuthorized session current transaction
|
||||
|
||||
-- 对象和权限复核后提交事务差分。
|
||||
commitAuthorized :: Session -> Text -> Transaction -> IO (Either SessionError QueryResult)
|
||||
commitAuthorized session current transaction = do
|
||||
backend <- currentBackend session
|
||||
liveResult <- beSnapshot backend
|
||||
case liveResult of
|
||||
Left err -> pure (Left (engineErrorCode err))
|
||||
Right live -> let diffs = transactionDiffs (txBase transaction) (txStaged transaction) live in case transactionConflict diffs of
|
||||
Just msg -> pure (Left (SessionError "serialization_failure" (T.pack msg)))
|
||||
Nothing -> do
|
||||
applied <- beApplyTransaction backend (transactionOps diffs)
|
||||
case applied of
|
||||
Left err -> pure (Left (engineErrorCode err))
|
||||
Right () -> do
|
||||
writeIORef (ssTransaction session) Nothing
|
||||
pure (Right (emptyResult (Just current)))
|
||||
commitAuthorized session current transaction
|
||||
| Just token <- txToken transaction = commitSecurity (T.unpack current) (commitMvcc session current transaction token)
|
||||
| otherwise = commitLegacy session current transaction
|
||||
|
||||
-- 在短提交门内校验最新合并行集并发布版本。
|
||||
commitMvcc :: Session -> Text -> Transaction -> String -> IO (Either SessionError QueryResult)
|
||||
commitMvcc session current transaction token = do
|
||||
backend <- currentBackend session
|
||||
preview <- bePreviewTransaction backend token
|
||||
case preview of
|
||||
Left err -> pure (Left (engineErrorCode err))
|
||||
Right (checkpoint, candidate) -> do
|
||||
let modified = changedTables (txBase transaction) (txStaged transaction)
|
||||
names = modified ++ domainDependencies (txBase transaction) modified
|
||||
dependencies = [(name, table) | (name, table) <- txBase transaction, name `elem` names]
|
||||
checked = validateDependencies dependencies candidate >> mapM_ (validateFinal candidate) modified
|
||||
case checked of
|
||||
Left err -> pure (Left (SessionError "serialization_failure" (T.pack err)))
|
||||
Right () -> do
|
||||
result <- beCommitTransaction backend token checkpoint
|
||||
case result of
|
||||
Left err -> pure (Left (engineErrorCode err))
|
||||
Right () -> do
|
||||
writeIORef (ssTransaction session) Nothing
|
||||
pure (Right (emptyResult (Just current)))
|
||||
where
|
||||
-- 复用 SQL 最终行集约束,避免存储重复规则。
|
||||
validateFinal candidate name = maybe (Left "transaction table missing") (Engine.validateTableRows candidate) (lookup name candidate)
|
||||
|
||||
-- 内存后端保留原差分提交与冲突行为。
|
||||
commitLegacy :: Session -> Text -> Transaction -> IO (Either SessionError QueryResult)
|
||||
commitLegacy session current transaction = do
|
||||
backend <- currentBackend session
|
||||
liveResult <- beSnapshot backend
|
||||
case liveResult of
|
||||
Left err -> pure (Left (engineErrorCode err))
|
||||
Right live -> do
|
||||
let base = txBase transaction
|
||||
diffs = transactionDiffs base (txStaged transaction) live
|
||||
modified =
|
||||
[ name
|
||||
| (name, diff) <- diffs
|
||||
, not (null (tdDeletes diff) && null (tdWrites diff)) || isJust (tdReplace diff)
|
||||
]
|
||||
readTables = if txSerializable transaction && not (null modified) then nub (map lastTablePart (concatMap queryTables (txStatements transaction))) else []
|
||||
names = modified ++ readTables ++ domainDependencies base (modified ++ readTables)
|
||||
dependencies = [(name, table) | (name, table) <- base, name `elem` names]
|
||||
ops = transactionOps diffs
|
||||
checked = do
|
||||
validateDependencies dependencies live
|
||||
maybe (Right ()) Left (transactionConflict diffs)
|
||||
mapM_ (validateRead base live) readTables
|
||||
candidate <- applyTransactionMemory ops live
|
||||
mapM_ (validateFinal candidate) modified
|
||||
case checked of
|
||||
Left msg -> pure (Left (SessionError "serialization_failure" (T.pack msg)))
|
||||
Right () -> do
|
||||
applied <- beApplyTransaction backend ops
|
||||
case applied of
|
||||
Left err -> pure (Left (engineErrorCode err))
|
||||
Right () -> do
|
||||
writeIORef (ssTransaction session) Nothing
|
||||
pure (Right (emptyResult (Just current)))
|
||||
where
|
||||
-- 拒绝读过的表出现数据变化或幻行。
|
||||
validateRead base live name = case (lookup name base, lookup name live) of
|
||||
(Just before, Just after) | tableRows before == tableRows after -> Right ()
|
||||
_ -> Left ("serializable transaction read changed: " ++ name)
|
||||
-- 复用引擎规则检查合并后的最终行集。
|
||||
validateFinal candidate name = case lookup name candidate of
|
||||
Nothing -> Left ("transaction table missing: " ++ name)
|
||||
Just table -> Engine.validateTableRows candidate table
|
||||
|
||||
-- 检查当前数据库连接权限。
|
||||
sessionConnectAllowed :: Session -> IO (Either SessionError ())
|
||||
@@ -412,9 +521,11 @@ sessionConnectAllowed session = do
|
||||
snapshotObjectIds :: Session -> [String] -> IO (Either SessionError [(String, A.Value)])
|
||||
snapshotObjectIds session names = do
|
||||
database <- readIORef (ssCurrent session)
|
||||
if database == "system" then pure (Right []) else do
|
||||
results <- mapM (one database) (filter (not . T.isPrefixOf "__system_" . T.pack) names)
|
||||
pure (sequence results)
|
||||
if database == "system"
|
||||
then pure (Right [])
|
||||
else do
|
||||
results <- mapM (one database) (filter (not . T.isPrefixOf "__system_" . T.pack) names)
|
||||
pure (sequence results)
|
||||
where
|
||||
-- 解析一个快照表的稳定对象编号。
|
||||
one database name = do
|
||||
@@ -431,9 +542,14 @@ rollbackTransaction session = do
|
||||
existing <- readIORef (ssTransaction session)
|
||||
case existing of
|
||||
Nothing -> pure (Left (SessionError "query_error" "no transaction in progress"))
|
||||
Just _ -> do
|
||||
writeIORef (ssTransaction session) Nothing
|
||||
pure (Right (emptyResult (Just current)))
|
||||
Just transaction -> do
|
||||
backend <- currentBackend session
|
||||
result <- maybe (pure (Right ())) (beRollbackTransaction backend) (txToken transaction)
|
||||
case result of
|
||||
Left err -> pure (Left (engineErrorCode err))
|
||||
Right () -> do
|
||||
writeIORef (ssTransaction session) Nothing
|
||||
pure (Right (emptyResult (Just current)))
|
||||
|
||||
-- | 建保存点:把事务当前状态压进保存点栈
|
||||
savepointTransaction :: Session -> Text -> IO (Either SessionError QueryResult)
|
||||
@@ -444,7 +560,7 @@ savepointTransaction session name = do
|
||||
Nothing -> pure (Left (SessionError "query_error" "no transaction in progress"))
|
||||
Just transaction -> do
|
||||
let stack = txSavepoints transaction ++ [(T.unpack name, txStaged transaction, txStatements transaction)]
|
||||
writeIORef (ssTransaction session) (Just transaction {txSavepoints = stack})
|
||||
writeIORef (ssTransaction session) (Just transaction{txSavepoints = stack})
|
||||
pure (Right (emptyResult (Just current)))
|
||||
|
||||
-- | 回滚到保存点:事务状态退回保存点,该保存点保留
|
||||
@@ -458,10 +574,13 @@ rollbackToSavepoint session name = do
|
||||
Nothing -> pure (Left (SessionError "query_error" ("no such savepoint: " <> name)))
|
||||
Just index -> do
|
||||
let (_, staged, statements) = txSavepoints transaction !! index
|
||||
writeIORef
|
||||
(ssTransaction session)
|
||||
(Just transaction {txStaged = staged, txStatements = statements, txSavepoints = take (index + 1) (txSavepoints transaction)})
|
||||
pure (Right (emptyResult (Just current)))
|
||||
let next = transaction{txStaged = staged, txStatements = statements, txSavepoints = take (index + 1) (txSavepoints transaction)}
|
||||
stagedResult <- stageMvcc session next
|
||||
case stagedResult of
|
||||
Left err -> pure (Left err)
|
||||
Right () -> do
|
||||
writeIORef (ssTransaction session) (Just next)
|
||||
pure (Right (emptyResult (Just current)))
|
||||
|
||||
-- | 释放保存点:该保存点与它之后的保存点一起丢掉
|
||||
releaseSavepoint :: Session -> Text -> IO (Either SessionError QueryResult)
|
||||
@@ -473,7 +592,7 @@ releaseSavepoint session name = do
|
||||
Just transaction -> case savepointIndex name (txSavepoints transaction) of
|
||||
Nothing -> pure (Left (SessionError "query_error" ("no such savepoint: " <> name)))
|
||||
Just index -> do
|
||||
writeIORef (ssTransaction session) (Just transaction {txSavepoints = take index (txSavepoints transaction)})
|
||||
writeIORef (ssTransaction session) (Just transaction{txSavepoints = take index (txSavepoints transaction)})
|
||||
pure (Right (emptyResult (Just current)))
|
||||
|
||||
-- | 最近一次同名保存点的下标
|
||||
@@ -483,7 +602,7 @@ savepointIndex name entries =
|
||||
[] -> Nothing
|
||||
hits -> Just (last hits)
|
||||
|
||||
-- | 事务里跑一条数据语句:只动内存快照,不碰存储层
|
||||
-- | 执行事务数据语句并取得修改行锁
|
||||
runInTransaction :: Session -> Statement -> IO (Either SessionError QueryResult)
|
||||
runInTransaction session statement = do
|
||||
existing <- readIORef (ssTransaction session)
|
||||
@@ -493,8 +612,48 @@ runInTransaction session statement = do
|
||||
Just transaction -> case scopeDdl (T.unpack current) statement >>= Engine.runStatement (txStaged transaction) of
|
||||
Left err -> pure (Left (engineErrorCode err))
|
||||
Right (staged, rows) -> do
|
||||
writeIORef (ssTransaction session) (Just transaction {txStaged = staged, txStatements = statement : txStatements transaction})
|
||||
pure (Right (resultOfStaged (txStaged transaction) statement rows))
|
||||
let next = transaction{txStaged = staged, txStatements = statement : txStatements transaction}
|
||||
locked <- stageMvcc session next
|
||||
case locked of
|
||||
Left err -> pure (Left err)
|
||||
Right () -> do
|
||||
writeIORef (ssTransaction session) (Just next)
|
||||
pure (Right (resultOfStaged (txStaged transaction) statement rows))
|
||||
|
||||
-- 自动提交语句同样拥有快照和原子提交边界。
|
||||
runAutocommit :: Session -> Statement -> IO (Either SessionError QueryResult)
|
||||
runAutocommit session statement = do
|
||||
begun <- beginTransaction session False
|
||||
case begun of
|
||||
Left err -> pure (Left err)
|
||||
Right _ -> do
|
||||
executed <- runInTransaction session statement
|
||||
case executed of
|
||||
Left err -> do
|
||||
rolled <- rollbackTransaction session
|
||||
pure (const (Left err) =<< rolled)
|
||||
Right rows -> do
|
||||
committed <- commitTransaction session
|
||||
case committed of
|
||||
Right _ -> pure (Right rows)
|
||||
Left err -> do
|
||||
rolled <- rollbackTransaction session
|
||||
pure (const (Left err) =<< rolled)
|
||||
|
||||
-- 比较事务最终行集,保留原表顺序。
|
||||
changedTables :: Database -> Database -> [String]
|
||||
changedTables base staged = [name | (name, table) <- staged, fmap tableRows (lookup name base) /= Just (tableRows table)]
|
||||
|
||||
-- 获取本事务实际修改行的锁并更新读依赖。
|
||||
stageMvcc :: Session -> Transaction -> IO (Either SessionError ())
|
||||
stageMvcc session transaction = case txToken transaction of
|
||||
Nothing -> pure (Right ())
|
||||
Just token -> do
|
||||
backend <- currentBackend session
|
||||
let modified = changedTables (txBase transaction) (txStaged transaction)
|
||||
ops = [TxnReplace name (tableRows table) | (name, table) <- txStaged transaction, name `elem` modified]
|
||||
dependencies = nub (map lastTablePart (concatMap queryTables (txStatements transaction)))
|
||||
fmap (first engineErrorCode) (beStageTransaction backend token ops dependencies)
|
||||
|
||||
-- | 事务内的结果集:空结果回落到解析出来的列名
|
||||
resultOfStaged :: Database -> Statement -> [Row] -> QueryResult
|
||||
@@ -516,7 +675,7 @@ data TableDiff = TableDiff
|
||||
transactionOps :: [(String, TableDiff)] -> [TxnOp]
|
||||
transactionOps = concatMap opsOf
|
||||
where
|
||||
-- | 一张表的请求:先删后写
|
||||
-- \| 一张表的请求:先删后写
|
||||
opsOf (name, diff) =
|
||||
[TxnDelete name (tdDeletes diff) | not (null (tdDeletes diff))]
|
||||
++ [TxnUpsert name (tdWrites diff) | not (null (tdWrites diff))]
|
||||
@@ -533,16 +692,16 @@ transactionConflict diffs =
|
||||
transactionDiffs :: Database -> Database -> Database -> [(String, TableDiff)]
|
||||
transactionDiffs base staged live = [(name, diffFor name) | name <- names]
|
||||
where
|
||||
-- | 事务里碰得着的表:数据语句不增删表
|
||||
-- \| 事务里碰得着的表:数据语句不增删表
|
||||
names = nubKeys (map fst base ++ map fst staged)
|
||||
|
||||
-- | 一张表的差异
|
||||
-- \| 一张表的差异
|
||||
diffFor name
|
||||
| stagedRows == baseRows = emptyDiff
|
||||
| all (isJust . rowKey) (baseRows ++ stagedRows) = keyedDiff
|
||||
-- 动过,库里还是基线:整表替换
|
||||
| liveRows == baseRows = emptyDiff {tdReplace = Just stagedRows}
|
||||
| otherwise = emptyDiff {tdConflict = Just ("write conflict on table " ++ name)}
|
||||
| liveRows == baseRows = emptyDiff{tdReplace = Just stagedRows}
|
||||
| otherwise = emptyDiff{tdConflict = Just ("write conflict on table " ++ name)}
|
||||
where
|
||||
baseRows = rowsOf name base
|
||||
stagedRows = rowsOf name staged
|
||||
@@ -577,13 +736,13 @@ transactionDiffs base staged live = [(name, diffFor name) | name <- names]
|
||||
[] -> Nothing
|
||||
}
|
||||
|
||||
-- | 没有差异的空结果
|
||||
-- \| 没有差异的空结果
|
||||
emptyDiff = TableDiff [] [] Nothing Nothing
|
||||
|
||||
-- | 表名去重但保持顺序
|
||||
-- \| 表名去重但保持顺序
|
||||
nubKeys = foldr (\name acc -> if name `elem` acc then acc else name : acc) []
|
||||
|
||||
-- | 库里某张表的行
|
||||
-- \| 库里某张表的行
|
||||
rowsOf name db = maybe [] tableRows (lookup name db)
|
||||
|
||||
-- | 按整数 id 建立保留首行的查找表
|
||||
@@ -759,14 +918,31 @@ currentBackend session = do
|
||||
showIdentities :: Session -> IO (Either SessionError QueryResult)
|
||||
showIdentities session = do
|
||||
admin <- maybe False principalIsCatalogManager <$> readIORef (ssPrincipal session)
|
||||
if not admin then pure (Left (SessionError "forbidden" "catalog manager required")) else do
|
||||
result <- beAccounts (ssBackend session) ReqAccountsList
|
||||
pure $ case result of
|
||||
Left err -> Left (SessionError "storage_error" (T.pack err))
|
||||
Right identities -> Right (QueryResult ["id", "name", "can_login", "is_superuser", "enabled", "system_catalog_manager", "allow_sudo_auth", "created_at"]
|
||||
[[VInt (fromInteger (accountId a)), VStr (T.unpack (accountUser a)), VBool (accountCanLogin a),
|
||||
VBool (accountIsSuperuser a), VBool (accountEnabled a), VBool (accountSystemCatalogManager a), VBool (accountAllowSudoAuth a), VStr (T.unpack (accountRegisteredAt a))] | a <- identities]
|
||||
(length identities) False Nothing)
|
||||
if not admin
|
||||
then pure (Left (SessionError "forbidden" "catalog manager required"))
|
||||
else do
|
||||
result <- beAccounts (ssBackend session) ReqAccountsList
|
||||
pure $ case result of
|
||||
Left err -> Left (SessionError "storage_error" (T.pack err))
|
||||
Right identities ->
|
||||
Right
|
||||
( QueryResult
|
||||
["id", "name", "can_login", "is_superuser", "enabled", "system_catalog_manager", "allow_sudo_auth", "created_at"]
|
||||
[ [ VInt (fromInteger (accountId a))
|
||||
, VStr (T.unpack (accountUser a))
|
||||
, VBool (accountCanLogin a)
|
||||
, VBool (accountIsSuperuser a)
|
||||
, VBool (accountEnabled a)
|
||||
, VBool (accountSystemCatalogManager a)
|
||||
, VBool (accountAllowSudoAuth a)
|
||||
, VStr (T.unpack (accountRegisteredAt a))
|
||||
]
|
||||
| a <- identities
|
||||
]
|
||||
(length identities)
|
||||
False
|
||||
Nothing
|
||||
)
|
||||
|
||||
-- | 执行身份管理并通知受影响会话
|
||||
runAccount :: Session -> AccountCommand -> IO (Either SessionError QueryResult)
|
||||
@@ -849,10 +1025,9 @@ resultOf result =
|
||||
, qrDatabase = Nothing
|
||||
}
|
||||
where
|
||||
-- | 按列名取单元格
|
||||
-- \| 按列名取单元格
|
||||
cellOf row name = fromMaybe VNull (lookup name row)
|
||||
|
||||
|
||||
-- 展示当前登录身份自己的有效权限。
|
||||
showSelfGrants :: Session -> IO (Either SessionError QueryResult)
|
||||
showSelfGrants session = do
|
||||
@@ -863,10 +1038,15 @@ showSelfGrants session = do
|
||||
grants <- listSelfGrants (ssPrivileges session) principal
|
||||
pure $ case grants of
|
||||
Left err -> Left (privilegeErrorOf err)
|
||||
Right rows -> Right (QueryResult ["identity", "privilege", "object", "grant_option"]
|
||||
[[VStr (T.unpack (grantRole grant)), VStr (T.unpack (grantPrivilege grant)), VStr (T.unpack (grantObject grant)), VBool (grantable grant)] | grant <- rows]
|
||||
(length rows) False Nothing)
|
||||
|
||||
Right rows ->
|
||||
Right
|
||||
( QueryResult
|
||||
["identity", "privilege", "object", "grant_option"]
|
||||
[[VStr (T.unpack (grantRole grant)), VStr (T.unpack (grantPrivilege grant)), VStr (T.unpack (grantObject grant)), VBool (grantable grant)] | grant <- rows]
|
||||
(length rows)
|
||||
False
|
||||
Nothing
|
||||
)
|
||||
|
||||
-- 在连接视图及权限稳定期间分析范式。
|
||||
analyzeNormalizationCoded :: Session -> NormalizationRequest -> IO (Either SessionError A.Value)
|
||||
@@ -902,8 +1082,9 @@ normalizationView session table payload = do
|
||||
Left message -> pure (Left ("storage_error", message))
|
||||
Right identity -> case transaction of
|
||||
Just tx -> case (lookup (T.unpack table) (txStaged tx), lookup (T.unpack table) (txObjects tx)) of
|
||||
(Just relation, Just oldObject) | objectIdentity oldObject == Right identity ->
|
||||
pure (normalizationReport identity (map normalizationColumn (tableCols relation)) "transaction_snapshot" payload)
|
||||
(Just relation, Just oldObject)
|
||||
| objectIdentity oldObject == Right identity ->
|
||||
pure (normalizationReport identity (map normalizationColumn (tableCols relation)) "transaction_snapshot" payload)
|
||||
_ -> pure (Left ("conflict", "normalization snapshot object has changed"))
|
||||
Nothing -> do
|
||||
backend <- currentBackend session
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module ChuSQL.Server.TCP (
|
||||
ServerConfig (..),
|
||||
@@ -16,18 +16,17 @@ module ChuSQL.Server.TCP (
|
||||
isLocalPeer,
|
||||
) where
|
||||
|
||||
import qualified ChuSQL.Core.Engine.Error as E
|
||||
import ChuSQL.Core.Protocol (Account (..), Request (ReqIdentityInitialize))
|
||||
import ChuSQL.Interface.Config (ServerConfig (..), defaultServerConfig, loadServerConfigAt)
|
||||
import ChuSQL.Interface.Sudo (SudoCredential, createSudoCredential, removeSudoCredential, sudoChallenge, validSudoChallenge, isLocalPeer)
|
||||
import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||
import ChuSQL.Interface.Policy (PasswordPolicy (..))
|
||||
import ChuSQL.Interface.Protocol (ClientRequest (..), ServerResponse (..), decodeRequest, encodeResponse, protocolVersion)
|
||||
import ChuSQL.Interface.RateLimit (RateLimiter, rateLimitBlock, rateLimitClear, rateLimitRecord)
|
||||
import qualified ChuSQL.Core.Engine.Error as E
|
||||
import ChuSQL.Core.Protocol (Account (..), Request (ReqIdentityInitialize))
|
||||
import ChuSQL.Interface.Sudo (SudoCredential, createSudoCredential, isLocalPeer, removeSudoCredential, sudoChallenge, validSudoChallenge)
|
||||
import ChuSQL.Server.Backend (Backend (..))
|
||||
import ChuSQL.Server.Session (QueryResult (..), Session, SessionError (..), accounts, authenticateSessionCoded, authenticateSudoSessionCoded, isPlainIdentifier, catalog, databases, newSessionWith, policyOf, reloadPolicy, roleViews, analyzeNormalizationCoded, runStatementCoded, runStorageCoded, sessionIsAdmin, sessionUser)
|
||||
import ChuSQL.Server.Security (writeSecurity)
|
||||
import ChuSQL.Server.Privileges (newPrivileges, reconcileObjects)
|
||||
import ChuSQL.Server.Security (writeSecurity)
|
||||
import ChuSQL.Server.Session (QueryResult (..), Session, SessionError (..), accounts, analyzeNormalizationCoded, authenticateSessionCoded, authenticateSudoSessionCoded, catalog, closeSession, databases, isPlainIdentifier, newSessionWith, policyOf, reloadPolicy, roleViews, runStatementCoded, runStorageCoded, sessionIsAdmin, sessionUser)
|
||||
import Control.Concurrent (forkIO)
|
||||
import Control.Concurrent.MVar (MVar, modifyMVar_, newMVar, readMVar)
|
||||
import Control.Exception (IOException, bracket, finally, onException, try)
|
||||
@@ -39,6 +38,7 @@ import qualified Data.Map.Strict as Map
|
||||
import Data.Maybe (fromMaybe, isNothing)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||
import Network.Socket (
|
||||
AddrInfo (..),
|
||||
AddrInfoFlag (AI_PASSIVE),
|
||||
@@ -54,13 +54,13 @@ import Network.Socket (
|
||||
defaultHints,
|
||||
defaultProtocol,
|
||||
getAddrInfo,
|
||||
getSocketName,
|
||||
getPeerName,
|
||||
getSocketName,
|
||||
listen,
|
||||
setSocketOption,
|
||||
socket,
|
||||
socketToHandle,
|
||||
)
|
||||
)
|
||||
import System.IO (BufferMode (LineBuffering), Handle, IOMode (ReadWriteMode), hClose, hFlush, hPutStrLn, hSetBuffering, hSetNewlineMode, noNewlineTranslation, stderr)
|
||||
import System.IO.Error (eofErrorType, isEOFError, mkIOError)
|
||||
|
||||
@@ -106,11 +106,11 @@ dropConnectionsOf live names = do
|
||||
mapM_ markDropped hits
|
||||
pure (length hits)
|
||||
where
|
||||
-- | 这条连接登录的账号在名单里吗
|
||||
-- \| 这条连接登录的账号在名单里吗
|
||||
isOf wanted entry = do
|
||||
current <- readIORef (leWho entry)
|
||||
pure (maybe False (\name -> T.toLower name `elem` wanted) current)
|
||||
-- | 只做标记:连接在下一次请求应答后收工
|
||||
-- \| 只做标记:连接在下一次请求应答后收工
|
||||
markDropped entry = writeIORef (leDropped entry) True
|
||||
|
||||
-- 服务器
|
||||
@@ -163,10 +163,12 @@ runServer :: ServerEnv -> (Int -> IO ()) -> IO ()
|
||||
runServer env announce = bracket (listenSocket (scHost (srvConfig env)) (scPort (srvConfig env))) close $ \sock -> do
|
||||
port <- actualPort sock
|
||||
enableSudo env port
|
||||
(do
|
||||
announce port
|
||||
stopped <- newMVar False
|
||||
acceptLoop env sock stopped) `finally` disableSudo env
|
||||
( do
|
||||
announce port
|
||||
stopped <- newMVar False
|
||||
acceptLoop env sock stopped
|
||||
)
|
||||
`finally` disableSudo env
|
||||
|
||||
-- 为显式配置的本机身份映射创建凭据。
|
||||
enableSudo :: ServerEnv -> Int -> IO ()
|
||||
@@ -174,14 +176,15 @@ enableSudo env port
|
||||
| T.null user = pure ()
|
||||
| not (isPlainIdentifier user) = ioError (userError "sudo_auth_user must be a plain identity name")
|
||||
| otherwise = modifyMVar_ (srvSudo env) $ \case
|
||||
Just _
|
||||
-> ioError (userError "sudo authentication is already running")
|
||||
Nothing
|
||||
-> do credential <- createSudoCredential port
|
||||
hPutStrLn
|
||||
stderr
|
||||
("sudo_auth enabled: local elevated identity -> " ++ T.unpack user)
|
||||
pure (Just (port, user, credential))
|
||||
Just _ ->
|
||||
ioError (userError "sudo authentication is already running")
|
||||
Nothing ->
|
||||
do
|
||||
credential <- createSudoCredential port
|
||||
hPutStrLn
|
||||
stderr
|
||||
("sudo_auth enabled: local elevated identity -> " ++ T.unpack user)
|
||||
pure (Just (port, user, credential))
|
||||
where
|
||||
user = scSudoUser (srvConfig env)
|
||||
|
||||
@@ -227,7 +230,7 @@ actualPort sock = do
|
||||
acceptLoop :: ServerEnv -> Socket -> MVar Bool -> IO ()
|
||||
acceptLoop env sock stopped = loop
|
||||
where
|
||||
-- | 等下一个连接
|
||||
-- \| 等下一个连接
|
||||
loop = do
|
||||
stop <- readMVar stopped
|
||||
if stop
|
||||
@@ -260,13 +263,13 @@ serveConnection env conn = do
|
||||
entry <- newLiveEntry (isLocalPeer peer)
|
||||
unregister <- registerConnection (srvLive env) entry
|
||||
session <- newSessionWith (srvBackend env) (srvRootName env) (srvSettingsFile env) (void . dropConnectionsOf (srvLive env))
|
||||
outcome <- try (loop h session entry BSC.empty `finally` unregister) :: IO (Either IOException ())
|
||||
outcome <- try (loop h session entry BSC.empty `finally` (closeSession session `finally` unregister)) :: IO (Either IOException ())
|
||||
case outcome of
|
||||
Left err | not (isEOFError err) -> hPutStrLn stderr ("connection error: " ++ show err)
|
||||
_ -> pure ()
|
||||
void (try (hClose h) :: IO (Either IOException ()))
|
||||
where
|
||||
-- | 逐行读请求并应答
|
||||
-- \| 逐行读请求并应答
|
||||
loop h session entry pending = do
|
||||
frame <- readRequestLine h (scMaxMessage (srvConfig env)) pending
|
||||
case frame of
|
||||
@@ -281,7 +284,7 @@ serveConnection env conn = do
|
||||
(response, keepGoing) <- dispatch env session entry request
|
||||
send h response
|
||||
when keepGoing $ loop h session entry remaining
|
||||
-- | 发一条响应并冲缓冲
|
||||
-- \| 发一条响应并冲缓冲
|
||||
send h response = do
|
||||
BLC.hPutStr h (encodeResponse response)
|
||||
BLC.hPutStr h "\n"
|
||||
@@ -291,7 +294,7 @@ serveConnection env conn = do
|
||||
readRequestLine :: Handle -> Int -> BSC.ByteString -> IO (Maybe (BSC.ByteString, BSC.ByteString))
|
||||
readRequestLine handle limit = collect [] 0
|
||||
where
|
||||
-- | 按块读取至换行、超限或 EOF
|
||||
-- \| 按块读取至换行、超限或 EOF
|
||||
collect parts total bytes = case BSC.elemIndex '\n' bytes of
|
||||
Just offset -> finish (BSC.take offset bytes : parts) (BSC.drop (offset + 1) bytes)
|
||||
Nothing
|
||||
@@ -299,13 +302,14 @@ readRequestLine handle limit = collect [] 0
|
||||
| otherwise -> do
|
||||
next <- BSC.hGetSome handle (max 1 (min 4094 (limit - used) + 2))
|
||||
if BSC.null next
|
||||
then if used == 0
|
||||
then ioError (mkIOError eofErrorType "read request" (Just handle) Nothing)
|
||||
else finish (bytes : parts) BSC.empty
|
||||
then
|
||||
if used == 0
|
||||
then ioError (mkIOError eofErrorType "read request" (Just handle) Nothing)
|
||||
else finish (bytes : parts) BSC.empty
|
||||
else collect (bytes : parts) used next
|
||||
where
|
||||
used = total + BSC.length bytes
|
||||
-- | 合并并校验一行的有效长度
|
||||
-- \| 合并并校验一行的有效长度
|
||||
finish parts remaining = do
|
||||
let line = BSC.dropWhileEnd (== '\r') (BSC.concat (reverse parts))
|
||||
pure (if BSC.length line > limit then Nothing else Just (line, remaining))
|
||||
@@ -387,10 +391,13 @@ sudoLogin env session entry user proof = do
|
||||
who <- sessionUser session
|
||||
let key = T.toLower (T.strip user)
|
||||
mapped = maybe "" (\(_, name, _) -> name) configured
|
||||
valid = leLocal entry && isNothing who && key == mapped
|
||||
&& case configured of
|
||||
Just (_, _, credential) -> validSudoChallenge credential now challenge key proof
|
||||
Nothing -> False
|
||||
valid =
|
||||
leLocal entry
|
||||
&& isNothing who
|
||||
&& key == mapped
|
||||
&& case configured of
|
||||
Just (_, _, credential) -> validSudoChallenge credential now challenge key proof
|
||||
Nothing -> False
|
||||
blocked <- rateLimitBlock (srvLimiter env) key
|
||||
response <- case blocked of
|
||||
Just _ -> pure (RespError "too_many_attempts" "too many failed sudo logins")
|
||||
@@ -408,8 +415,15 @@ sudoLogin env session entry user proof = do
|
||||
writeIORef (leWho entry) (Just key)
|
||||
admin <- sessionIsAdmin session
|
||||
pure (RespLogin key admin)
|
||||
hPutStrLn stderr ("sudo_auth attempt: mapped_user=" ++ T.unpack mapped
|
||||
++ " local=" ++ show (leLocal entry) ++ " result=" ++ auditResult response)
|
||||
hPutStrLn
|
||||
stderr
|
||||
( "sudo_auth attempt: mapped_user="
|
||||
++ T.unpack mapped
|
||||
++ " local="
|
||||
++ show (leLocal entry)
|
||||
++ " result="
|
||||
++ auditResult response
|
||||
)
|
||||
pure response
|
||||
where
|
||||
-- 提取不包含凭据的认证审计结果。
|
||||
@@ -463,5 +477,5 @@ trimResult limit result
|
||||
| otherwise = result
|
||||
|
||||
-- | show 成 Text
|
||||
tshow :: Show a => a -> Text
|
||||
tshow :: (Show a) => a -> Text
|
||||
tshow = T.pack . show
|
||||
+361
-92
@@ -3,7 +3,6 @@
|
||||
|
||||
module Main (main) where
|
||||
|
||||
import Data.Either (lefts)
|
||||
import ChuSQL.Core.Engine.Storage.IPC (
|
||||
Account (..),
|
||||
Request (ReqAccountCreate, ReqAccountReset, ReqAccountsList, ReqIdentityAlter),
|
||||
@@ -13,20 +12,28 @@ import ChuSQL.Core.Engine.Storage.IPC (
|
||||
setStorageLink,
|
||||
)
|
||||
import ChuSQL.Core.Engine.Syntax.AST (Statement)
|
||||
import ChuSQL.Interface.Normalization
|
||||
import ChuSQL.Server.Normalization (normalizationBudget, normalizationReport, schemaFingerprint)
|
||||
import ChuSQL.Core.Protocol (SchemaColumn (..))
|
||||
import Data.IORef (newIORef, readIORef, writeIORef)
|
||||
import ChuSQL.Core.Engine.Syntax.Parser (parseStatement)
|
||||
import ChuSQL.Core.Model (Database, Histogram (..), Row, Table (..), TableIdentity (..), Value (..), pattern TInt, pattern TStr)
|
||||
import ChuSQL.Core.Protocol (SchemaColumn (..), TxnOp (..))
|
||||
import ChuSQL.Interface.Auth (hashPasswordWith)
|
||||
import ChuSQL.Interface.Protocol (ClientRequest (..), Grant (..), RoleView (..), ServerResponse (..), decodeRequest, encodeResponse, encodeRequest, decodeResponse)
|
||||
import ChuSQL.Interface.Link (clientQuery, clientSudoLogin, closeClient, connectClient)
|
||||
import ChuSQL.Interface.Normalization
|
||||
import ChuSQL.Interface.Protocol (ClientRequest (..), Grant (..), RoleView (..), ServerResponse (..), decodeRequest, decodeResponse, encodeRequest, encodeResponse)
|
||||
import ChuSQL.Interface.RateLimit (newRateLimiter)
|
||||
import ChuSQL.Interface.Link (connectClient, closeClient, clientSudoLogin, clientQuery)
|
||||
import ChuSQL.Interface.Sudo (SudoCredential, newSudoCredential, sudoProof, verifySudoProof, validSudoChallenge,
|
||||
createSudoCredential, removeSudoCredential, readSudoCredential, sudoPrivileged)
|
||||
import ChuSQL.Interface.Sudo (
|
||||
SudoCredential,
|
||||
createSudoCredential,
|
||||
newSudoCredential,
|
||||
readSudoCredential,
|
||||
removeSudoCredential,
|
||||
sudoPrivileged,
|
||||
sudoProof,
|
||||
validSudoChallenge,
|
||||
verifySudoProof,
|
||||
)
|
||||
import ChuSQL.Server.Accounts (Principal (..), ensureRootAccount)
|
||||
import ChuSQL.Server.Backend (Backend (..), StatementResult (..), ipcBackend, memoryBackend)
|
||||
import ChuSQL.Server.Backend (Backend (..), StatementResult (..), applyTransactionMemory, ipcBackend, memoryBackend)
|
||||
import ChuSQL.Server.Normalization (normalizationBudget, normalizationReport, schemaFingerprint)
|
||||
import ChuSQL.Server.Privileges (
|
||||
PrivilegeCommand (..),
|
||||
PrivilegeError (..),
|
||||
@@ -35,29 +42,28 @@ import ChuSQL.Server.Privileges (
|
||||
authorize,
|
||||
authorizeConnect,
|
||||
claimObject,
|
||||
prepareObject,
|
||||
listRoleViews,
|
||||
listSelfGrants,
|
||||
newPrivileges,
|
||||
prepareObject,
|
||||
runPrivilegeCommand,
|
||||
)
|
||||
import ChuSQL.Server.Session (QueryResult (..), Session, SessionError (..), newSession, runStatementCoded, runStorageCoded, authenticateSessionCoded)
|
||||
import ChuSQL.Server.Security (databaseSecurity, writeSecurity)
|
||||
import ChuSQL.Server.Session (QueryResult (..), Session, SessionError (..), authenticateSessionCoded, newSession, runStatementCoded, runStorageCoded)
|
||||
import ChuSQL.Server.TCP (
|
||||
ServerConfig (..),
|
||||
ServerEnv (..),
|
||||
ServerHandle (..),
|
||||
defaultServerConfig,
|
||||
isLocalPeer,
|
||||
loadServerConfigAt,
|
||||
newServerEnv,
|
||||
startServer,
|
||||
isLocalPeer,
|
||||
)
|
||||
import Control.Concurrent (forkIO, forkFinally, killThread, threadDelay)
|
||||
import Control.Concurrent (forkFinally, forkIO, killThread, threadDelay)
|
||||
import Control.Concurrent.MVar (MVar, modifyMVar_, newEmptyMVar, newMVar, putMVar, takeMVar)
|
||||
import System.Timeout (timeout)
|
||||
import Control.Exception (IOException, bracket, finally, try)
|
||||
import Control.Monad (replicateM_, void, (>=>), when)
|
||||
import Data.List (sort)
|
||||
import Control.Exception (IOException, bracket, evaluate, finally, try)
|
||||
import Control.Monad (forM_, replicateM_, void, when, (>=>))
|
||||
import Data.Aeson (decode)
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.Aeson.Key as K
|
||||
@@ -65,16 +71,17 @@ import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Char8 as BSC
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import Data.Either (lefts)
|
||||
import Data.IORef (atomicModifyIORef', newIORef, readIORef, writeIORef)
|
||||
import Data.List (sort)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Vector as V
|
||||
import Data.Time.Clock (addUTCTime, diffUTCTime, getCurrentTime)
|
||||
import qualified Data.Vector as V
|
||||
import Network.Socket (
|
||||
AddrInfo (..),
|
||||
SockAddr (..),
|
||||
tupleToHostAddress,
|
||||
tupleToHostAddress6,
|
||||
SocketType (Stream),
|
||||
addrAddress,
|
||||
addrFamily,
|
||||
@@ -84,6 +91,8 @@ import Network.Socket (
|
||||
getAddrInfo,
|
||||
socket,
|
||||
socketToHandle,
|
||||
tupleToHostAddress,
|
||||
tupleToHostAddress6,
|
||||
withSocketsDo,
|
||||
)
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist, getTemporaryDirectory, removeFile, removePathForcibly)
|
||||
@@ -99,6 +108,8 @@ import System.IO (
|
||||
hSetNewlineMode,
|
||||
noNewlineTranslation,
|
||||
)
|
||||
import System.Mem.StableName (makeStableName)
|
||||
import System.Timeout (timeout)
|
||||
import Test.Hspec hiding (after, before)
|
||||
|
||||
-- TCP 服务器测试:协议编解码、配置读取与真连服务器的端到端用例。
|
||||
@@ -129,16 +140,19 @@ spec = do
|
||||
connected <- connectClient "127.0.0.1" (shPort handle)
|
||||
case connected of
|
||||
Left message -> expectationFailure (T.unpack message)
|
||||
Right client -> (do
|
||||
clientSudoLogin client credential "local_user" >>= (`shouldBe` Right False)
|
||||
result <- clientQuery client "select 1"
|
||||
result `shouldSatisfy` isRightResult) `finally` closeClient client
|
||||
Right client ->
|
||||
( do
|
||||
clientSudoLogin client credential "local_user" >>= (`shouldBe` Right False)
|
||||
result <- clientQuery client "select 1"
|
||||
result `shouldSatisfy` isRightResult
|
||||
)
|
||||
`finally` closeClient client
|
||||
it "only treats kernel loopback addresses as local" $ do
|
||||
isLocalPeer (SockAddrInet 7777 (tupleToHostAddress (127, 0, 0, 1))) `shouldBe` True
|
||||
isLocalPeer (SockAddrInet 7777 (tupleToHostAddress (192, 168, 1, 2))) `shouldBe` False
|
||||
isLocalPeer (SockAddrInet6 7777 0 (tupleToHostAddress6 (0,0,0,0,0,0,0,1)) 0) `shouldBe` True
|
||||
isLocalPeer (SockAddrInet6 7777 0 (tupleToHostAddress6 (0,0,0,0,0,65535,32512,1)) 0) `shouldBe` True
|
||||
isLocalPeer (SockAddrInet6 7777 0 (tupleToHostAddress6 (0,0,0,0,0,65535,49320,1)) 0) `shouldBe` False
|
||||
isLocalPeer (SockAddrInet6 7777 0 (tupleToHostAddress6 (0, 0, 0, 0, 0, 0, 0, 1)) 0) `shouldBe` True
|
||||
isLocalPeer (SockAddrInet6 7777 0 (tupleToHostAddress6 (0, 0, 0, 0, 0, 65535, 32512, 1)) 0) `shouldBe` True
|
||||
isLocalPeer (SockAddrInet6 7777 0 (tupleToHostAddress6 (0, 0, 0, 0, 0, 65535, 49320, 1)) 0) `shouldBe` False
|
||||
it "binds proofs to the instance, challenge, account and expiration" $ do
|
||||
credential <- newSudoCredential
|
||||
other <- newSudoCredential
|
||||
@@ -153,18 +167,19 @@ spec = do
|
||||
validSudoChallenge credential now Nothing "local_user" proof `shouldBe` False
|
||||
it "requires an OS elevated identity to provision or read credentials" $ do
|
||||
privileged <- sudoPrivileged
|
||||
if privileged then do
|
||||
bracket (createSudoCredential 65534) (const (removeSudoCredential 65534)) $ \credential -> do
|
||||
stored <- readSudoCredential 65534
|
||||
case stored of
|
||||
Left message -> expectationFailure (T.unpack message)
|
||||
Right restored -> sudoProof restored "native" "local_user" `shouldBe` sudoProof credential "native" "local_user"
|
||||
duplicate <- try (createSudoCredential 65534) :: IO (Either IOException SudoCredential)
|
||||
duplicate `shouldSatisfy` isLeftResult
|
||||
else do
|
||||
created <- try (createSudoCredential 65534) :: IO (Either IOException SudoCredential)
|
||||
created `shouldSatisfy` isLeftResult
|
||||
readSudoCredential 65534 >>= (`shouldSatisfy` isLeftResult)
|
||||
if privileged
|
||||
then do
|
||||
bracket (createSudoCredential 65534) (const (removeSudoCredential 65534)) $ \credential -> do
|
||||
stored <- readSudoCredential 65534
|
||||
case stored of
|
||||
Left message -> expectationFailure (T.unpack message)
|
||||
Right restored -> sudoProof restored "native" "local_user" `shouldBe` sudoProof credential "native" "local_user"
|
||||
duplicate <- try (createSudoCredential 65534) :: IO (Either IOException SudoCredential)
|
||||
duplicate `shouldSatisfy` isLeftResult
|
||||
else do
|
||||
created <- try (createSudoCredential 65534) :: IO (Either IOException SudoCredential)
|
||||
created `shouldSatisfy` isLeftResult
|
||||
readSudoCredential 65534 >>= (`shouldSatisfy` isLeftResult)
|
||||
it "logs in an ordinary mapped identity without elevating its privileges" $
|
||||
withTestSudo $ \_ handle credential -> withConnection handle $ \client -> do
|
||||
answer <- tcpSudo client credential "local_user"
|
||||
@@ -202,7 +217,8 @@ spec = do
|
||||
it "rejects disabled and NOLOGIN identities" $
|
||||
withTestSudo $ \_ handle credential -> withConnection handle $ \admin -> do
|
||||
_ <- tcpLogin admin testRootName testRootPassword
|
||||
mapM_ (checkSudoDisabled admin handle credential)
|
||||
mapM_
|
||||
(checkSudoDisabled admin handle credential)
|
||||
["alter user local_user disabled", "alter user local_user enabled nologin"]
|
||||
it "does not inherit sudo authentication from a role" $
|
||||
withTestSudo $ \_ handle credential -> withConnection handle $ \admin -> do
|
||||
@@ -224,6 +240,7 @@ spec = do
|
||||
tcpServerSpec
|
||||
privilegeSpec
|
||||
objectPrivilegeSpec
|
||||
databaseGateSpec
|
||||
transactionSpec
|
||||
ipcConcurrencySpec
|
||||
identitySpec
|
||||
@@ -245,9 +262,10 @@ identitySpec = describe "unified identities" $ do
|
||||
asText (at "status" shown) `shouldBe` "result"
|
||||
switched <- tcpQuery manager "use system"
|
||||
asText (at "status" switched) `shouldBe` "result"
|
||||
mapM_ (tcpQuery manager
|
||||
>=>
|
||||
(\ reply -> asText (at "code" reply) `shouldBe` "forbidden"))
|
||||
mapM_
|
||||
( tcpQuery manager
|
||||
>=> (\reply -> asText (at "code" reply) `shouldBe` "forbidden")
|
||||
)
|
||||
["alter user manager superuser", "alter user managed system_catalog_manager", "alter user managed allow_sudo_auth", "alter user admin identified by 'Another-Pass123!'", "create database forbidden"]
|
||||
it "does not inherit catalog management through role membership" $ withTcpServer defaultServerConfig $ \_ handle ->
|
||||
withConnection handle $ \admin -> withConnection handle $ \worker -> do
|
||||
@@ -312,9 +330,10 @@ identitySpec = describe "unified identities" $ do
|
||||
it "protects the last enabled login superuser" $ withTcpServer defaultServerConfig $ \_ handle ->
|
||||
withConnection handle $ \h -> do
|
||||
_ <- tcpLogin h "admin" "s3cret"
|
||||
mapM_ (tcpQuery h
|
||||
>=>
|
||||
(\ reply -> asText (at "code" reply) `shouldBe` "bad_request"))
|
||||
mapM_
|
||||
( tcpQuery h
|
||||
>=> (\reply -> asText (at "code" reply) `shouldBe` "bad_request")
|
||||
)
|
||||
["alter user admin disabled", "alter role admin nologin", "alter user admin nosuperuser", "drop user admin", "drop role admin"]
|
||||
shown <- tcpQuery h "show roles"
|
||||
asText (at "status" shown) `shouldBe` "result"
|
||||
@@ -383,22 +402,28 @@ testDatabaseName = "test"
|
||||
-- | users 5 行 + orders 1 行
|
||||
testDb :: Database
|
||||
testDb =
|
||||
[ ( "users"
|
||||
, Table
|
||||
[
|
||||
( "users"
|
||||
, Table
|
||||
"users"
|
||||
[("id", TInt), ("name", TStr), ("age", TInt)]
|
||||
[ [("id", VInt i), ("name", VStr ("user" ++ show i)), ("age", VInt (20 + i))]
|
||||
| i <- [1 .. 5]
|
||||
]
|
||||
Nothing Nothing Nothing
|
||||
)
|
||||
, ( "orders"
|
||||
, Table
|
||||
Nothing
|
||||
Nothing
|
||||
Nothing
|
||||
)
|
||||
,
|
||||
( "orders"
|
||||
, Table
|
||||
"orders"
|
||||
[("id", TInt), ("user_id", TInt)]
|
||||
[[("id", VInt 1), ("user_id", VInt 1)]]
|
||||
Nothing Nothing Nothing
|
||||
)
|
||||
Nothing
|
||||
Nothing
|
||||
Nothing
|
||||
)
|
||||
]
|
||||
|
||||
-- | 测试管理员名字
|
||||
@@ -466,8 +491,8 @@ tempSettingsPath name = do
|
||||
removeIfExists :: FilePath -> IO ()
|
||||
removeIfExists path = do
|
||||
there <- doesFileExist path
|
||||
when there
|
||||
$ void (try (removeFile path) :: IO (Either IOException ()))
|
||||
when there $
|
||||
void (try (removeFile path) :: IO (Either IOException ()))
|
||||
|
||||
-- | 起一台内存后端的测试服务器:管理员已设口令 `s3cret`
|
||||
withTcpServer :: ServerConfig -> (ServerEnv -> ServerHandle -> IO a) -> IO a
|
||||
@@ -493,11 +518,11 @@ withTcpServerAs password config body = do
|
||||
withConnection :: ServerHandle -> (Handle -> IO a) -> IO a
|
||||
withConnection handle = bracket open closeQuietly
|
||||
where
|
||||
-- | 关连接,忽略异常
|
||||
-- \| 关连接,忽略异常
|
||||
closeQuietly h = do
|
||||
_ <- try (hClose h) :: IO (Either IOException ())
|
||||
pure ()
|
||||
-- | 建连接并配成行缓冲句柄
|
||||
-- \| 建连接并配成行缓冲句柄
|
||||
open = withSocketsDo $ do
|
||||
infos <- getAddrInfo (Just defaultHints{addrSocketType = Stream}) (Just "127.0.0.1") (Just (show (shPort handle)))
|
||||
info <- case infos of
|
||||
@@ -1023,6 +1048,9 @@ privilegeSpec = describe "server privileges" $ do
|
||||
|
||||
it "requires administrator privileges for domain commands" $ withPrivileges $ \service root -> do
|
||||
authorizeSql service root "CREATE DOMAIN code AS INT" `shouldReturn` Right ()
|
||||
authorizeSql service root "ALTER DOMAIN code SET NOT NULL" `shouldReturn` Right ()
|
||||
authorizeSql service (Ordinary testAccount) "ALTER DOMAIN code SET NOT NULL"
|
||||
`shouldReturn` Left (PrivilegeError "forbidden" "administrator required")
|
||||
authorizeSql service (Ordinary testAccount) "CREATE DOMAIN code AS INT"
|
||||
`shouldReturn` Left (PrivilegeError "forbidden" "administrator required")
|
||||
|
||||
@@ -1071,8 +1099,11 @@ privilegeSpec = describe "server privileges" $ do
|
||||
_ <- runPrivilegeCommand service root testDatabase (CreateRoleCommand "reader")
|
||||
_ <- runPrivilegeCommand service root testDatabase (GrantPrivilegesCommand ["select"] "users" "reader" False)
|
||||
_ <- runPrivilegeCommand service root testDatabase (GrantRoleCommand "reader" ["alice"])
|
||||
mapM_ (\sql -> authorizeSql service (Ordinary testAccount) sql
|
||||
`shouldReturn` Left (PrivilegeError "forbidden" "permission denied: SELECT ON orders"))
|
||||
mapM_
|
||||
( \sql ->
|
||||
authorizeSql service (Ordinary testAccount) sql
|
||||
`shouldReturn` Left (PrivilegeError "forbidden" "permission denied: SELECT ON orders")
|
||||
)
|
||||
[ "SELECT COUNT(*) FROM users HAVING COUNT(*) > (SELECT COUNT(*) FROM orders)"
|
||||
, "SELECT name FROM users ORDER BY (SELECT COUNT(*) FROM orders) + id"
|
||||
]
|
||||
@@ -1172,8 +1203,9 @@ objectPrivilegeSpec = describe "stable object privileges" $ do
|
||||
mustRun (beStatement (beWithDatabase backend "test") "CREATE TABLE personal_owned (id INT)")
|
||||
claimObject service (Ordinary testAccount) testDatabase "personal_owned" `shouldReturn` Right ()
|
||||
grants <- listSelfGrants service (Ordinary testAccount)
|
||||
fmap (filter ((== "test.personal_owned") . grantObject)) grants `shouldBe` Right
|
||||
[Grant "alice" privilege "test.personal_owned" True | privilege <- ["select", "insert", "update", "delete"]]
|
||||
fmap (filter ((== "test.personal_owned") . grantObject)) grants
|
||||
`shouldBe` Right
|
||||
[Grant "alice" privilege "test.personal_owned" True | privilege <- ["select", "insert", "update", "delete"]]
|
||||
grants `shouldSatisfy` either (const False) (not . any ((== "test.users") . grantObject))
|
||||
|
||||
it "checks DISTINCT table reads and nested subqueries" $ withObjectPrivileges $ \_ service root -> do
|
||||
@@ -1217,10 +1249,16 @@ objectPrivilegeSpec = describe "stable object privileges" $ do
|
||||
release <- newEmptyMVar
|
||||
finished <- newEmptyMVar
|
||||
revoked <- newEmptyMVar
|
||||
let slow = backend{beWithDatabase = \name -> (beWithDatabase backend name){beStatement = \sql -> do
|
||||
when (sql == "SELECT * FROM users")
|
||||
$ putMVar started () >> takeMVar release
|
||||
beStatement (beWithDatabase backend name) sql}}
|
||||
let slow =
|
||||
backend
|
||||
{ beWithDatabase = \name ->
|
||||
(beWithDatabase backend name)
|
||||
{ beStatement = \sql -> do
|
||||
when (sql == "SELECT * FROM users") $
|
||||
putMVar started () >> takeMVar release
|
||||
beStatement (beWithDatabase backend name) sql
|
||||
}
|
||||
}
|
||||
settings <- tempSettingsPath "permission-race"
|
||||
reader <- newSession slow testRootName settings
|
||||
admin <- newSession backend testRootName settings
|
||||
@@ -1528,9 +1566,159 @@ failureCode = either sessCode (const "")
|
||||
intValues :: [[Value]] -> [Int]
|
||||
intValues rows = [n | row <- rows, Just (VInt n) <- [nth 0 row]]
|
||||
|
||||
-- 数据库执行门的并行、等待与取消回归。
|
||||
databaseGateSpec :: Spec
|
||||
databaseGateSpec = describe "database execution gates" $ do
|
||||
it "lets another database write while preserving same-database exclusion" $ do
|
||||
entered <- newEmptyMVar
|
||||
release <- newEmptyMVar
|
||||
done <- newEmptyMVar
|
||||
bracket (forkFinally (databaseSecurity [("gate_a", True)] (putMVar entered () >> takeMVar release)) (putMVar done)) killThread $ \_ -> do
|
||||
takeMVar entered
|
||||
timeout 1000000 (databaseSecurity [("gate_b", True)] (pure ())) `shouldReturn` Just ()
|
||||
timeout 50000 (databaseSecurity [("gate_a", False)] (pure ())) `shouldReturn` Nothing
|
||||
timeout 50000 (writeSecurity (pure ())) `shouldReturn` Nothing
|
||||
putMVar release ()
|
||||
result <- timeout 1000000 (takeMVar done)
|
||||
case result of Just (Right ()) -> pure (); _ -> expectationFailure "database writer failed to finish"
|
||||
timeout 1000000 (databaseSecurity [("gate_a", True)] (pure ())) `shouldReturn` Just ()
|
||||
timeout 1000000 (writeSecurity (pure ())) `shouldReturn` Just ()
|
||||
|
||||
it "orders opposite dependency lists and releases cancelled waiters" $ do
|
||||
entered <- newEmptyMVar
|
||||
release <- newEmptyMVar
|
||||
done <- newEmptyMVar
|
||||
bracket (forkFinally (databaseSecurity [("gate_y", True), ("gate_x", False)] (putMVar entered () >> takeMVar release)) (putMVar done)) killThread $ \_ -> do
|
||||
takeMVar entered
|
||||
timeout 50000 (databaseSecurity [("gate_x", True), ("gate_y", False)] (pure ())) `shouldReturn` Nothing
|
||||
putMVar release ()
|
||||
result <- timeout 1000000 (takeMVar done)
|
||||
case result of Just (Right ()) -> pure (); _ -> expectationFailure "ordered writer failed to finish"
|
||||
timeout 1000000 (databaseSecurity [("gate_x", True), ("gate_y", True)] (pure ())) `shouldReturn` Just ()
|
||||
|
||||
-- | 显式事务用例
|
||||
transactionSpec :: Spec
|
||||
transactionSpec = describe "server transactions" $ do
|
||||
it "previews legacy duplicate deletion exactly as storage does" $ do
|
||||
let old = Table "legacy" [("id", TInt), ("name", TStr)] [[("id", VInt 1), ("name", VStr "first")], [("id", VInt 1), ("name", VStr "second")]] Nothing Nothing Nothing
|
||||
case applyTransactionMemory [TxnDelete "legacy" [1, 1]] [("legacy", old)] of
|
||||
Left err -> expectationFailure err
|
||||
Right db -> fmap tableRows (lookup "legacy" db) `shouldBe` Just [[("id", VInt 1), ("name", VStr "second")]]
|
||||
|
||||
it "rechecks UNIQUE on merged rows before publishing any table" $ withTwoSessions $ \first second -> do
|
||||
_ <- mustSql first "CREATE TABLE registrations (id INT PRIMARY KEY, username TEXT UNIQUE)"
|
||||
_ <- mustSql first "BEGIN"
|
||||
_ <- mustSql second "BEGIN"
|
||||
_ <- mustSql first "INSERT INTO registrations (id, username) VALUES (1, 'same')"
|
||||
_ <- mustSql second "INSERT INTO registrations (id, username) VALUES (2, 'same')"
|
||||
_ <- mustSql second "UPDATE users SET age = 99 WHERE id = 1"
|
||||
_ <- mustSql first "COMMIT"
|
||||
denied <- runStatementCoded second "COMMIT"
|
||||
failureCode denied `shouldBe` "serialization_failure"
|
||||
failureMessage denied `shouldBe` "UNIQUE: duplicate value in column username"
|
||||
_ <- mustSql second "ROLLBACK"
|
||||
sessionAge first 1 `shouldReturn` Just 21
|
||||
rows <- mustSql first "SELECT id FROM registrations"
|
||||
qrRows rows `shouldBe` [[VInt 1]]
|
||||
|
||||
it "allows distinct unique values and multiple NULLs after merging" $ withTwoSessions $ \first second -> do
|
||||
_ <- mustSql first "CREATE TABLE registrations (id INT PRIMARY KEY, username TEXT UNIQUE)"
|
||||
forM_ [first, second] $ \session -> void (mustSql session "BEGIN")
|
||||
_ <- mustSql first "INSERT INTO registrations (id, username) VALUES (1, 'one'), (3, NULL)"
|
||||
_ <- mustSql second "INSERT INTO registrations (id, username) VALUES (2, 'two'), (4, NULL)"
|
||||
_ <- mustSql first "COMMIT"
|
||||
_ <- mustSql second "COMMIT"
|
||||
rows <- mustSql first "SELECT COUNT(*) FROM registrations"
|
||||
qrRows rows `shouldBe` [[VInt 4]]
|
||||
|
||||
forM_ ["SELECT id FROM test.users WHERE id = 1", "SELECT id FROM users WHERE EXISTS (SELECT id FROM users WHERE age > 0)"] $ \readSql ->
|
||||
it ("protects serializable read dependencies: " ++ T.unpack readSql) $ withTwoSessions $ \first second -> do
|
||||
_ <- mustSql first "CREATE TABLE actions (id INT PRIMARY KEY)"
|
||||
_ <- mustSql first "BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE"
|
||||
_ <- mustSql first readSql
|
||||
_ <- mustSql first "INSERT INTO actions (id) VALUES (1)"
|
||||
_ <- mustSql second "INSERT INTO users (id, name, age) VALUES (7, 'new', 20)"
|
||||
denied <- runStatementCoded first "COMMIT"
|
||||
failureCode denied `shouldBe` "serialization_failure"
|
||||
_ <- mustSql first "ROLLBACK"
|
||||
rows <- mustSql second "SELECT COUNT(*) FROM actions"
|
||||
qrRows rows `shouldBe` [[VInt 0]]
|
||||
|
||||
it "forgets rolled-back reads and ignores unrelated table changes" $ withTwoSessions $ \first second -> do
|
||||
_ <- mustSql first "CREATE TABLE actions (id INT PRIMARY KEY)"
|
||||
_ <- mustSql first "START TRANSACTION ISOLATION LEVEL SERIALIZABLE"
|
||||
_ <- mustSql first "SAVEPOINT before_read"
|
||||
_ <- mustSql first "SELECT * FROM users"
|
||||
_ <- mustSql first "ROLLBACK TO before_read"
|
||||
_ <- mustSql first "INSERT INTO actions (id) VALUES (1)"
|
||||
_ <- mustSql second "UPDATE users SET age = 99 WHERE id = 1"
|
||||
_ <- mustSql first "COMMIT"
|
||||
rows <- mustSql second "SELECT COUNT(*) FROM actions"
|
||||
qrRows rows `shouldBe` [[VInt 1]]
|
||||
|
||||
it "allows a serializable read-only snapshot to finish" $ withTwoSessions $ \first second -> do
|
||||
_ <- mustSql first "BEGIN ISOLATION LEVEL SERIALIZABLE"
|
||||
_ <- mustSql first "SELECT * FROM users"
|
||||
_ <- mustSql second "UPDATE users SET age = 99 WHERE id = 1"
|
||||
void (mustSql first "COMMIT")
|
||||
|
||||
forM_ [("another commit", True), ("an autocommit update", False)] $ \(label, transactional) ->
|
||||
it ("excludes " ++ label ++ " between conflict checking and publication") $ do
|
||||
db <- newMVar testDb
|
||||
settings <- tempSettingsPath "transaction-publication"
|
||||
removeIfExists settings
|
||||
armed <- newIORef False
|
||||
snapshots <- newIORef (0 :: Int)
|
||||
captured <- newEmptyMVar
|
||||
release <- newEmptyMVar
|
||||
secondSnapshot <- newEmptyMVar
|
||||
let
|
||||
-- 在首次提交取得快照后暂停发布。
|
||||
pausedBackend original =
|
||||
original
|
||||
{ beWithDatabase = pausedBackend . beWithDatabase original
|
||||
, beStatement = \sql -> do
|
||||
enabled <- readIORef armed
|
||||
when (enabled && not transactional && sql == "UPDATE users SET age = 32 WHERE id = 1") (putMVar secondSnapshot ())
|
||||
beStatement original sql
|
||||
, beSnapshot = do
|
||||
result <- beSnapshot original
|
||||
enabled <- readIORef armed
|
||||
when enabled $ do
|
||||
n <- atomicModifyIORef' snapshots (\count -> (count + 1, count))
|
||||
when (n == 0) (putMVar captured () >> takeMVar release)
|
||||
when (n == 1) (putMVar secondSnapshot ())
|
||||
pure result
|
||||
}
|
||||
backend = pausedBackend (memoryBackend testDatabaseName db)
|
||||
first <- newSession backend (T.pack testDatabaseName) settings
|
||||
second <- newSession backend (T.pack testDatabaseName) settings
|
||||
mapM_ (`mustSql` "USE test") [first, second]
|
||||
_ <- mustSql first "BEGIN"
|
||||
when transactional $ do
|
||||
_ <- mustSql second "BEGIN"
|
||||
void (mustSql second "UPDATE users SET age = 32 WHERE id = 1")
|
||||
_ <- mustSql first "UPDATE users SET age = 31 WHERE id = 1"
|
||||
writeIORef armed True
|
||||
firstDone <- newEmptyMVar
|
||||
secondDone <- newEmptyMVar
|
||||
bracket (forkFinally (runStatementCoded first "COMMIT") (putMVar firstDone)) killThread $ \_ -> do
|
||||
timeout 2000000 (takeMVar captured) `shouldReturn` Just ()
|
||||
bracket (forkFinally (runStatementCoded second (if transactional then "COMMIT" else "UPDATE users SET age = 32 WHERE id = 1")) (putMVar secondDone)) killThread $ \_ -> do
|
||||
entered <- timeout 200000 (takeMVar secondSnapshot)
|
||||
putMVar release ()
|
||||
firstResult <- timeout 2000000 (takeMVar firstDone)
|
||||
secondResult <- timeout 2000000 (takeMVar secondDone)
|
||||
entered `shouldBe` Nothing
|
||||
case firstResult of
|
||||
Just (Right result) -> failureCode result `shouldBe` ""
|
||||
_ -> expectationFailure "first commit did not complete"
|
||||
case secondResult of
|
||||
Just (Right result) -> failureCode result `shouldBe` (if transactional then "serialization_failure" else "")
|
||||
_ -> expectationFailure "second commit did not complete"
|
||||
when transactional $ void (mustSql second "ROLLBACK")
|
||||
sessionAge first 1 `shouldReturn` Just (if transactional then 31 else 32)
|
||||
|
||||
it "executes DISTINCT against the connection snapshot with output columns" $ withMemorySession $ \session -> do
|
||||
_ <- mustSql session "CREATE TABLE distinct_values (id INT, value INT)"
|
||||
_ <- mustSql session "INSERT INTO distinct_values (id,value) VALUES (1,7),(2,7),(3,8)"
|
||||
@@ -1575,7 +1763,7 @@ transactionSpec = describe "server transactions" $ do
|
||||
updated <- beStatement backend "UPDATE users SET id = 3"
|
||||
void updated `shouldBe` Right ()
|
||||
snapshot <- beSnapshot (memoryBackend "other" ref)
|
||||
fmap (map (tableRows . snd)) snapshot `shouldBe` Right [[[ ("id", VInt 2) ]]]
|
||||
fmap (map (tableRows . snd)) snapshot `shouldBe` Right [[[("id", VInt 2)]]]
|
||||
it "keeps catalog and snapshot identities stable across system views and replacements" $ do
|
||||
ref <- newMVar [("test.users", Table "users" [("id", TInt)] [] Nothing Nothing Nothing)]
|
||||
let backend = memoryBackend "test" ref
|
||||
@@ -1601,9 +1789,11 @@ transactionSpec = describe "server transactions" $ do
|
||||
fmap (map tiSchemaVersion) current `shouldBe` Right [Just 3]
|
||||
fmap (map (tableSchemaVersion . snd)) snapshot `shouldBe` Right [Just 3]
|
||||
it "rejects cross-database reads and writes including nested queries" $ withMemorySession $ \session -> do
|
||||
mapM_ (\query -> do
|
||||
result <- runStatementCoded session query
|
||||
failureMessage result `shouldSatisfy` T.isInfixOf "outside the current database")
|
||||
mapM_
|
||||
( \query -> do
|
||||
result <- runStatementCoded session query
|
||||
failureMessage result `shouldSatisfy` T.isInfixOf "outside the current database"
|
||||
)
|
||||
[ "SELECT DISTINCT age FROM other.users"
|
||||
, "SELECT * FROM other.users"
|
||||
, "UPDATE other.users SET age = 1"
|
||||
@@ -1632,6 +1822,8 @@ transactionSpec = describe "server transactions" $ do
|
||||
_ <- mustSql session "create domain code as int"
|
||||
_ <- mustSql session "create table typed (id code)"
|
||||
_ <- mustSql session "begin"
|
||||
alterDenied <- runStatementCoded session "alter domain code set not null"
|
||||
failureMessage alterDenied `shouldBe` "DDL is not allowed in a transaction"
|
||||
denied <- runStatementCoded session "drop domain code"
|
||||
failureMessage denied `shouldBe` "DDL is not allowed in a transaction"
|
||||
_ <- mustSql session "insert into typed (id) values (7)"
|
||||
@@ -1639,6 +1831,24 @@ transactionSpec = describe "server transactions" $ do
|
||||
rows <- mustSql session "select id from typed"
|
||||
intValues (qrRows rows) `shouldBe` [7]
|
||||
|
||||
it "rejects stale transaction writes after a domain constraint changes" $ withTwoSessions $ \writer administrator -> do
|
||||
void (mustSql administrator "CREATE DOMAIN quantity AS INT")
|
||||
void (mustSql administrator "CREATE DOMAIN nested AS quantity")
|
||||
void (mustSql administrator "CREATE TABLE domain_items (id INT, qty nested)")
|
||||
void (mustSql writer "BEGIN")
|
||||
void (mustSql writer "INSERT INTO domain_items (id, qty) VALUES (1, 0)")
|
||||
void (mustSql administrator "ALTER DOMAIN quantity ADD CHECK (VALUE > 0)")
|
||||
refused <- runStatementCoded writer "COMMIT"
|
||||
failureCode refused `shouldBe` "serialization_failure"
|
||||
void (mustSql writer "ROLLBACK")
|
||||
rows <- mustSql administrator "SELECT qty FROM domain_items"
|
||||
qrRows rows `shouldBe` []
|
||||
void (mustSql writer "BEGIN")
|
||||
void (mustSql writer "INSERT INTO domain_items (id, qty) VALUES (2, 2)")
|
||||
void (mustSql writer "COMMIT")
|
||||
saved <- mustSql administrator "SELECT qty FROM domain_items"
|
||||
intValues (qrRows saved) `shouldBe` [2]
|
||||
|
||||
it "keeps uncommitted rows invisible to other sessions until COMMIT" $ withTwoSessions $ \writer reader -> do
|
||||
_ <- mustSql writer "begin"
|
||||
_ <- mustSql writer "insert into users (id, name, age) values (6, 'six', 26)"
|
||||
@@ -1684,11 +1894,12 @@ transactionSpec = describe "server transactions" $ do
|
||||
ref <- newMVar testDb
|
||||
settings <- tempSettingsPath "snapshot-failure"
|
||||
let base = memoryBackend testDatabaseName ref
|
||||
-- | 保留测试后端并让快照读取失败
|
||||
failing name = (beWithDatabase base name)
|
||||
{ beSnapshot = pure (Left "data page unreadable")
|
||||
, beWithDatabase = failing
|
||||
}
|
||||
-- \| 保留测试后端并让快照读取失败
|
||||
failing name =
|
||||
(beWithDatabase base name)
|
||||
{ beSnapshot = pure (Left "data page unreadable")
|
||||
, beWithDatabase = failing
|
||||
}
|
||||
session <- newSession (failing testDatabaseName) (T.pack testDatabaseName) settings
|
||||
void (mustSql session "USE test")
|
||||
result <- runStatementCoded session "BEGIN"
|
||||
@@ -1708,12 +1919,15 @@ transactionSpec = describe "server transactions" $ do
|
||||
_ <- mustSql session "CREATE TABLE constrained_updates (id INT PRIMARY KEY, qty INT NOT NULL UNIQUE)"
|
||||
_ <- mustSql session "INSERT INTO constrained_updates (id, qty) VALUES (1, 2), (2, 3)"
|
||||
_ <- mustSql session "BEGIN"
|
||||
mapM_ (\(query, marker) -> do
|
||||
result <- runStatementCoded session query
|
||||
failureMessage result `shouldSatisfy` T.isInfixOf marker)
|
||||
[("UPDATE constrained_updates SET qty = NULL WHERE id = 1", "NOT NULL")
|
||||
mapM_
|
||||
( \(query, marker) -> do
|
||||
result <- runStatementCoded session query
|
||||
failureMessage result `shouldSatisfy` T.isInfixOf marker
|
||||
)
|
||||
[ ("UPDATE constrained_updates SET qty = NULL WHERE id = 1", "NOT NULL")
|
||||
, ("UPDATE constrained_updates SET qty = 3 WHERE id = 1", "UNIQUE")
|
||||
, ("UPDATE constrained_updates SET id = 2 WHERE id = 1", "UNIQUE")]
|
||||
, ("UPDATE constrained_updates SET id = 2 WHERE id = 1", "UNIQUE")
|
||||
]
|
||||
unchanged <- mustSql session "SELECT qty FROM constrained_updates ORDER BY id"
|
||||
intValues (qrRows unchanged) `shouldBe` [2, 3]
|
||||
_ <- mustSql session "UPDATE constrained_updates SET qty = qty + 1"
|
||||
@@ -2005,6 +2219,26 @@ ipcConcurrencySpec = describe "server backend on the real storage" $ do
|
||||
intColumn "id" rowsA `shouldMatchList` [1 .. 20]
|
||||
intColumn "id" rowsB `shouldMatchList` [101 .. 120]
|
||||
|
||||
it "shares a committed snapshot and keeps old versions immutable" $ withIpcStorage $ do
|
||||
base <- ipcBackend
|
||||
mustRun (beStatement base "CREATE DATABASE alpha")
|
||||
let alpha = beWithDatabase base "alpha"
|
||||
mustRun (beStatement alpha "CREATE TABLE versions (id INT PRIMARY KEY, value INT)")
|
||||
mustRun (beStatement alpha "INSERT INTO versions (id, value) VALUES (1, 10)")
|
||||
first <- beSnapshot alpha >>= either fail pure
|
||||
second <- beSnapshot alpha >>= either fail pure
|
||||
firstName <- evaluate first >>= makeStableName
|
||||
secondName <- evaluate second >>= makeStableName
|
||||
(firstName == secondName) `shouldBe` True
|
||||
mustRun (beStatement alpha "UPDATE versions SET value = 20 WHERE id = 1")
|
||||
latest <- beSnapshot alpha >>= either fail pure
|
||||
fmap tableRows (lookup "versions" first) `shouldBe` Just [[("id", VInt 1), ("value", VInt 10)]]
|
||||
fmap tableRows (lookup "versions" latest) `shouldBe` Just [[("id", VInt 1), ("value", VInt 20)]]
|
||||
mustRun (beStatement base "DROP DATABASE alpha")
|
||||
mustRun (beStatement base "CREATE DATABASE alpha")
|
||||
recreated <- beSnapshot alpha >>= either fail pure
|
||||
map fst recreated `shouldNotContain` ["versions"]
|
||||
|
||||
it "commits an explicit transaction to the real storage" $ withIpcStorage $ do
|
||||
base <- ipcBackend
|
||||
mustRun (beStatement base "CREATE DATABASE alpha")
|
||||
@@ -2042,6 +2276,29 @@ ipcConcurrencySpec = describe "server backend on the real storage" $ do
|
||||
rows <- rowsOf (beStatement alpha "SELECT id FROM t")
|
||||
intColumn "id" rows `shouldBe` [1]
|
||||
|
||||
it "merges disjoint row writes while retaining the old snapshot" $ withIpcStorage $ do
|
||||
base <- ipcBackend
|
||||
mustRun (beStatement base "CREATE DATABASE alpha")
|
||||
let alpha = beWithDatabase base "alpha"
|
||||
mustRun (beStatement alpha "CREATE TABLE items (id int, value int)")
|
||||
mustRun (beStatement alpha "INSERT INTO items (id, value) VALUES (1, 10), (2, 20)")
|
||||
settings <- tempSettingsPath "transaction-ipc-rows"
|
||||
removeIfExists settings
|
||||
first <- newSession alpha "alpha" settings
|
||||
second <- newSession alpha "alpha" settings
|
||||
_ <- mustSql first "use alpha"
|
||||
_ <- mustSql second "use alpha"
|
||||
_ <- mustSql first "begin"
|
||||
_ <- mustSql second "begin"
|
||||
_ <- mustSql first "update items set value = 11 where id = 1"
|
||||
_ <- mustSql second "update items set value = 22 where id = 2"
|
||||
_ <- mustSql first "commit"
|
||||
old <- mustSql second "select value from items where id = 1"
|
||||
intValues (qrRows old) `shouldBe` [10]
|
||||
_ <- mustSql second "commit"
|
||||
rows <- rowsOf (beStatement alpha "SELECT value FROM items ORDER BY id")
|
||||
intColumn "value" rows `shouldBe` [11, 22]
|
||||
|
||||
it "refuses the second COMMIT on the real storage" $ withIpcStorage $ do
|
||||
base <- ipcBackend
|
||||
mustRun (beStatement base "CREATE DATABASE alpha")
|
||||
@@ -2057,17 +2314,15 @@ ipcConcurrencySpec = describe "server backend on the real storage" $ do
|
||||
_ <- mustSql first "begin"
|
||||
_ <- mustSql second "begin"
|
||||
_ <- mustSql first "update users set age = 31 where id = 1"
|
||||
_ <- mustSql second "update users set age = 42 where id = 1"
|
||||
_ <- mustSql first "commit"
|
||||
denied <- runStatementCoded second "commit"
|
||||
denied <- runStatementCoded second "update users set age = 42 where id = 1"
|
||||
failureCode denied `shouldBe` "serialization_failure"
|
||||
failureMessage denied `shouldBe` "write conflict on table users, id 1"
|
||||
failureMessage denied `shouldSatisfy` T.isInfixOf "row version changed"
|
||||
_ <- mustSql second "rollback"
|
||||
rows <- rowsOf (beStatement alpha "SELECT id, age FROM users")
|
||||
intColumn "id" rows `shouldBe` [1]
|
||||
intColumn "age" rows `shouldBe` [31]
|
||||
|
||||
|
||||
-- 判断明确错误返回。
|
||||
isLeftResult :: Either a b -> Bool
|
||||
isLeftResult (Left _) = True
|
||||
@@ -2094,8 +2349,16 @@ tcpSudo client credential user = do
|
||||
|
||||
-- 发送本机认证证明。
|
||||
tcpSudoProof :: Handle -> Text -> Text -> IO A.Value
|
||||
tcpSudoProof client user proof = tcpTalk client (BL.toStrict (A.encode (A.object
|
||||
["method" A..= ("sudo_login" :: Text), "user" A..= user, "proof" A..= proof])))
|
||||
tcpSudoProof client user proof =
|
||||
tcpTalk
|
||||
client
|
||||
( BL.toStrict
|
||||
( A.encode
|
||||
( A.object
|
||||
["method" A..= ("sudo_login" :: Text), "user" A..= user, "proof" A..= proof]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
-- 验证身份禁用或取消登录后拒绝本机认证。
|
||||
checkSudoDisabled :: Handle -> ServerHandle -> SudoCredential -> Text -> IO ()
|
||||
@@ -2111,7 +2374,6 @@ isRightResult :: Either a b -> Bool
|
||||
isRightResult (Right _) = True
|
||||
isRightResult _ = False
|
||||
|
||||
|
||||
-- 专用范式协议、权限、快照与预算回归。
|
||||
normalizationSpec :: Spec
|
||||
normalizationSpec = describe "normalization endpoint" $ do
|
||||
@@ -2127,8 +2389,9 @@ normalizationSpec = describe "normalization endpoint" $ do
|
||||
let response = RespNormalization report
|
||||
decodeResponse (encodeResponse response) `shouldBe` Right response
|
||||
decodeResponse (encodeResponse (RespNormalization (A.object ["conditional" A..= True]))) `shouldSatisfy` either (const True) (const False)
|
||||
mapM_ (\raw -> decodeNormalizationJSON raw `shouldSatisfy` either (T.isInfixOf "duplicate") (const False))
|
||||
[ "{\"a\":1,\"a\":2}", "{\"a\":1,\"\\u0061\":2}", "{\"a\":[{\"b\":1,\"b\":2}]}" ]
|
||||
mapM_
|
||||
(\raw -> decodeNormalizationJSON raw `shouldSatisfy` either (T.isInfixOf "duplicate") (const False))
|
||||
["{\"a\":1,\"a\":2}", "{\"a\":1,\"\\u0061\":2}", "{\"a\":[{\"b\":1,\"b\":2}]}"]
|
||||
let unknown = A.object ["api_version" A..= (1 :: Int), "database" A..= ("test" :: Text), "table" A..= ("users" :: Text), "declaration" A..= nrDeclaration normalizationPayload, "ignored" A..= True]
|
||||
(A.fromJSON unknown :: A.Result NormalizationRequest) `shouldSatisfy` isJSONFailure
|
||||
decodeRequest (BL.toStrict (encodeRequest (ReqNormalization normalizationPayload{nrVersion = 2}))) `shouldSatisfy` either (const True) (const False)
|
||||
@@ -2222,8 +2485,14 @@ normalizationSpec = describe "normalization endpoint" $ do
|
||||
|
||||
-- 共用明确声明的用户表请求。
|
||||
normalizationPayload :: NormalizationRequest
|
||||
normalizationPayload = NormalizationRequest 1 "test" "users"
|
||||
(NormalizationDeclaration [["id"]] [NormalizationDependency ["id"] ["name", "age"]] True (Just True)) Nothing Nothing
|
||||
normalizationPayload =
|
||||
NormalizationRequest
|
||||
1
|
||||
"test"
|
||||
"users"
|
||||
(NormalizationDeclaration [["id"]] [NormalizationDependency ["id"] ["name", "age"]] True (Just True))
|
||||
Nothing
|
||||
Nothing
|
||||
|
||||
-- 判断 JSON 类型校验失败。
|
||||
isJSONFailure :: A.Result a -> Bool
|
||||
|
||||
Reference in new issue
Block a user