门禁拆分与提速

This commit is contained in:
LIm1tZz7 committed 2026-10-03 23:16:21 +08:00
1 parent 03a1f2bfb2
commit 59027684e6
11 files changed
+642 -22

No files matched your search

@@ -7,6 +7,7 @@ module ChuSQL.Server.Privileges (
PrivilegeError (..),
Privileges,
RoleView (..),
affectedAccounts,
authorize,
authorizeTables,
filterTables,
@@ -111,6 +112,33 @@ runPrivilegeCommand service principal database command
RevokePrivilegesCommand _ object _ -> T.strip object /= "*"
_ -> False
-- | 一条命令会影响的账号:角色连它的成员一起展开,账号名原样留下
affectedAccounts :: Privileges -> PrivilegeCommand -> IO [Text]
affectedAccounts service command = do
known <- roleNames service
case known of
Left _ -> pure (direct command)
Right roles -> walk roles [] (direct command)
where
-- | 命令直接点到的名字
direct cmd = case cmd of
CreateRoleCommand _ -> []
DropRoleCommand role -> [role]
GrantPrivilegesCommand _ _ role _ -> [role]
RevokePrivilegesCommand _ _ role -> [role]
GrantRoleCommand _ members -> members
RevokeRoleCommand _ members -> members
-- | 是角色就接着往下走它的成员,走过的名字不再走第二遍
walk roles seen [] = pure (reverse seen)
walk roles seen (name : rest)
| name `elem` seen = walk roles seen rest
| normalizeRole name `elem` roles = do
members <- membersOfRole service name
case members of
Left _ -> walk roles (name : seen) rest
Right found -> walk roles (name : seen) (found ++ rest)
| otherwise = walk roles (name : seen) rest
-- | 语句鉴权:管理员全通,普通身份按需查
authorize :: Privileges -> Principal -> Text -> Statement -> IO (Either PrivilegeError ())
authorize service principal database stmt
+34 -7
View File
@@ -6,6 +6,7 @@ module ChuSQL.Server.Session
, SessionError (..)
, engineErrorCode
, newSession
, newSessionWith
, sessionUser
, sessionIsAdmin
, sessionDatabase
@@ -36,7 +37,7 @@ import ChuSQL.Interface.Auth (defaultSessionPolicy, newSessionStore)
import ChuSQL.Server.Accounts
import ChuSQL.Server.Backend (Backend (..), StatementResult (..), columnsFromStatement)
import ChuSQL.Server.Policy (configurePasswordPolicy)
import ChuSQL.Server.Privileges (PrivilegeCommand, PrivilegeError (..), Privileges, RoleView, authorize, filterTables, listRoleViews, newPrivileges, privilegeCommand, runPrivilegeCommand)
import ChuSQL.Server.Privileges (PrivilegeCommand, PrivilegeError (..), Privileges, RoleView, affectedAccounts, authorize, filterTables, listRoleViews, newPrivileges, privilegeCommand, runPrivilegeCommand)
import Control.Exception (IOException, try)
import Data.Bifunctor (first)
import Data.Char (isAlpha, isAlphaNum)
@@ -67,6 +68,7 @@ data Session = Session
, ssPrincipal :: IORef (Maybe Principal)
, ssSettingsFile :: FilePath
, ssTransaction :: IORef (Maybe Transaction)
, ssChanged :: [Text] -> IO ()
}
-- | 一条会话里的显式事务:开事务时的快照、事务内改到的状态与保存点栈
@@ -78,7 +80,11 @@ data Transaction = Transaction
-- | 开一个会话:账号与角色服务、配置策略都和 Web 端一致
newSession :: Backend -> Text -> FilePath -> IO Session
newSession backend name settingsFile = do
newSession backend name settingsFile = newSessionWith backend name settingsFile (const (pure ()))
-- | 开会话并接上账号变更通知回调
newSessionWith :: Backend -> Text -> FilePath -> ([Text] -> IO ()) -> IO Session
newSessionWith backend name settingsFile changed = do
sessions <- newSessionStore getCurrentTime defaultSessionPolicy
accountsService <- newAccounts backend sessions name
privileges <- newPrivileges backend
@@ -87,7 +93,7 @@ newSession backend name settingsFile = do
current <- newIORef ""
principal <- newIORef Nothing
transaction <- newIORef Nothing
pure (Session backend accountsService privileges current principal settingsFile transaction)
pure (Session backend accountsService privileges current principal settingsFile transaction changed)
-- | 当前生效的口令策略(只有管理员能看)
policyOf :: Session -> IO (Either Text PasswordPolicy)
@@ -529,7 +535,7 @@ currentBackend session = do
name <- readIORef (ssCurrent session)
pure (beWithDatabase (ssBackend session) (T.unpack name))
-- | 账号语句走账号服务,普通账号一律拒绝
-- | 账号语句走账号服务,普通账号一律拒绝;改完通知受影响账号
runAccount :: Session -> AccountCommand -> IO (Either SessionError QueryResult)
runAccount session command = do
who <- readIORef (ssPrincipal session)
@@ -537,9 +543,20 @@ runAccount session command = do
Nothing -> pure (Left (SessionError "unauthorized" "sign in first"))
Just principal -> do
result <- runAccountCommand (ssAccounts session) principal command
pure (either (Left . accountErrorOf) (const (Right (emptyResult Nothing))) result)
case result of
Left err -> pure (Left (accountErrorOf err))
Right () -> do
notifyChanged session (accountCommandUsers command)
pure (Right (emptyResult Nothing))
-- | 角色语句走权限服务,普通账号拒绝
-- | 一条账号命令影响的账号
accountCommandUsers :: AccountCommand -> [Text]
accountCommandUsers command = case command of
CreateAccount _ _ -> []
ResetAccountPassword name _ -> [name]
DropAccount name -> [name]
-- | 角色语句走权限服务,普通账号拒绝;改完通知受影响账号
runPrivilege :: Session -> PrivilegeCommand -> IO (Either SessionError QueryResult)
runPrivilege session command = do
who <- readIORef (ssPrincipal session)
@@ -547,8 +564,18 @@ runPrivilege session command = do
Nothing -> pure (Left (SessionError "unauthorized" "sign in first"))
Just principal -> do
database <- readIORef (ssCurrent session)
-- 受影响账号要趁命令落地之前算:删角色之后成员就查不到了
affected <- affectedAccounts (ssPrivileges session) command
result <- runPrivilegeCommand (ssPrivileges session) principal database command
pure (either (Left . privilegeErrorOf) (const (Right (emptyResult Nothing))) result)
case result of
Left err -> pure (Left (privilegeErrorOf err))
Right () -> do
notifyChanged session affected
pure (Right (emptyResult Nothing))
-- | 通知变更:把受影响的账号交给会话的变更钩子
notifyChanged :: Session -> [Text] -> IO ()
notifyChanged session names = ssChanged session (filter (not . T.null) names)
-- | 账号错误转成会话错误
accountErrorOf :: AccountError -> SessionError
+78 -15
View File
@@ -5,6 +5,8 @@ module ChuSQL.Server.TCP (
defaultServerConfig,
loadServerConfigAt,
ServerEnv (..),
LiveConnections,
newLiveConnections,
newServerEnv,
ServerHandle (..),
startServer,
@@ -17,13 +19,15 @@ import ChuSQL.Interface.Protocol (ClientRequest (..), ServerResponse (..), decod
import ChuSQL.Interface.RateLimit (RateLimiter, rateLimitBlock, rateLimitClear, rateLimitRecord)
import qualified ChuSQL.Core.Engine.Error as E
import ChuSQL.Server.Backend (Backend (..))
import ChuSQL.Server.Session (QueryResult (..), Session, SessionError (..), accounts, authenticateSessionCoded, catalog, databases, newSession, policyOf, reloadPolicy, roleViews, runStatementCoded, sessionIsAdmin, sessionUser)
import ChuSQL.Server.Session (QueryResult (..), Session, SessionError (..), accounts, authenticateSessionCoded, catalog, databases, newSessionWith, policyOf, reloadPolicy, roleViews, runStatementCoded, sessionIsAdmin, sessionUser)
import Control.Concurrent (forkIO)
import Control.Concurrent.MVar (MVar, modifyMVar_, newMVar, readMVar)
import Control.Exception (IOException, try)
import Control.Monad (void)
import Control.Exception (IOException, finally, try)
import Control.Monad (filterM, void)
import qualified Data.ByteString.Char8 as BSC
import qualified Data.ByteString.Lazy.Char8 as BLC
import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef, writeIORef)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
@@ -45,6 +49,8 @@ import Network.Socket (
getSocketName,
listen,
setSocketOption,
shutdown,
ShutdownCmd (ShutdownBoth),
socket,
socketToHandle,
)
@@ -53,6 +59,41 @@ import System.IO.Error (isEOFError)
-- TCP 服务器:监听端口,一连接一会话,按一行一 JSON 收发协议消息。
-- 在线连接与服务器环境
-- | 在线连接登记:账号名加一个「关掉这条连接」的动作
data LiveConnections = LiveConnections
{ lcNext :: IORef Int
, lcLive :: MVar (Map.Map Int (IORef (Maybe Text), IO ()))
}
-- | 建一张空的在线连接登记表
newLiveConnections :: IO LiveConnections
newLiveConnections = LiveConnections <$> newIORef 0 <*> newMVar Map.empty
-- | 登记一条连接,返回注销动作
registerConnection :: LiveConnections -> IORef (Maybe Text) -> IO () -> IO (IO ())
registerConnection live who kick = do
n <- atomicModifyIORef' (lcNext live) (\i -> (i + 1, i))
modifyMVar_ (lcLive live) (pure . Map.insert n (who, kick))
pure (modifyMVar_ (lcLive live) (pure . Map.delete n))
-- | 断开这些账号的在线连接,返回断开的条数
dropConnectionsOf :: LiveConnections -> [Text] -> IO Int
dropConnectionsOf live names = do
let wanted = map T.toLower (filter (not . T.null) names)
entries <- readMVar (lcLive live)
hits <- filterM (isOf wanted) (Map.elems entries)
mapM_ (tryKick . snd) hits
pure (length hits)
where
-- | 这条连接登录的账号在名单里吗
isOf wanted (who, _) = do
current <- readIORef who
pure (maybe False (\name -> T.toLower name `elem` wanted) current)
-- | 踢人失败不当错误:对端可能已经自己走了
tryKick kick = void (try kick :: IO (Either IOException ()))
-- 服务器
data ServerEnv = ServerEnv
@@ -61,12 +102,14 @@ data ServerEnv = ServerEnv
, srvSettingsFile :: FilePath
, srvConfig :: ServerConfig
, srvLimiter :: RateLimiter
, srvLive :: LiveConnections
}
-- | 组装服务器环境:每个连接再各开一个会话
newServerEnv :: Backend -> Text -> FilePath -> ServerConfig -> RateLimiter -> IO ServerEnv
newServerEnv backend rootName settingsFile config limiter =
pure (ServerEnv backend rootName settingsFile config limiter)
newServerEnv backend rootName settingsFile config limiter = do
live <- newLiveConnections
pure (ServerEnv backend rootName settingsFile config limiter live)
data ServerHandle = ServerHandle
{ shPort :: Int
@@ -148,31 +191,48 @@ reportConnection env conn = do
Left err -> hPutStrLn stderr ("connection failed: " ++ show err)
Right () -> pure ()
-- | 一条连接上跨请求的状态:登录账号、已被断开与注销动作
data Connection = Connection
{ cnWho :: IORef (Maybe Text)
, cnDropped :: IORef Bool
}
-- | 一个连接一个会话:一行一条 JSON,读到 EOF 就收工
serveConnection :: ServerEnv -> Socket -> IO ()
serveConnection env conn = do
h <- socketToHandle conn ReadWriteMode
hSetBuffering h LineBuffering
hSetNewlineMode h noNewlineTranslation
session <- newSession (srvBackend env) (srvRootName env) (srvSettingsFile env)
outcome <- try (loop h session) :: IO (Either IOException ())
who <- newIORef Nothing
dropped <- newIORef False
let kick = do
writeIORef dropped True
void (try (shutdown conn ShutdownBoth) :: IO (Either IOException ()))
unregister <- registerConnection (srvLive env) who kick
let connection = Connection who dropped
session <- newSessionWith (srvBackend env) (srvRootName env) (srvSettingsFile env) (\names -> void (dropConnectionsOf (srvLive env) names))
outcome <- try (loop h session connection `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 = do
loop h session connection = do
line <- BSC.hGetLine h
let cleaned = BSC.dropWhileEnd (== '\r') line
if BSC.length cleaned > scMaxMessage (srvConfig env)
then send h (RespError "too_large" "request line too large") >> loop h session
then send h (RespError "too_large" "request line too large") >> loop h session connection
else case decodeRequest cleaned of
Left message -> send h (RespError "bad_request" message) >> loop h session
Left message -> send h (RespError "bad_request" message) >> loop h session connection
Right request -> do
(response, keepGoing) <- dispatch env session request
send h response
if keepGoing then loop h session else pure ()
expired <- readIORef (cnDropped connection)
if expired
then send h (RespError "unauthorized" "session was dropped by an account change")
else do
(response, keepGoing) <- dispatch env session connection request
send h response
if keepGoing then loop h session connection else pure ()
-- | 发一条响应并冲缓冲
send h response = do
BLC.hPutStr h (encodeResponse response)
@@ -180,13 +240,16 @@ serveConnection env conn = do
hFlush h
-- | 一条请求一条响应,quit 后停连接
dispatch :: ServerEnv -> Session -> ClientRequest -> IO (ServerResponse, Bool)
dispatch env session request = case request of
dispatch :: ServerEnv -> Session -> Connection -> ClientRequest -> IO (ServerResponse, Bool)
dispatch env session connection request = case request of
ReqHello version
| version == protocolVersion -> pure (RespHello protocolVersion, True)
| otherwise -> pure (RespError "bad_request" ("unsupported protocol version: " <> tshow version), True)
ReqLogin user password -> do
response <- login env session user password
case response of
RespLogin who _ -> writeIORef (cnWho connection) (Just who)
_ -> pure ()
pure (response, True)
ReqQuery sql -> do
who <- sessionUser session
+73
View File
@@ -23,6 +23,7 @@ import ChuSQL.Server.Privileges (
PrivilegeCommand (..),
PrivilegeError (..),
Privileges,
affectedAccounts,
authorize,
listRoleViews,
newPrivileges,
@@ -480,6 +481,67 @@ tcpServerSpec = do
blocked <- tcpQuery other "select * from users"
asText (at "code" blocked) `shouldBe` "unauthorized"
describe "TCP server: account changes drop live sessions" $ do
it "disconnects a member whose role was revoked" $
withTcpServer defaultServerConfig $ \_ handle ->
withConnection handle $ \admin -> do
_ <- tcpLogin admin "admin" "s3cret"
_ <- tcpQuery admin "create user bob identified by 'bob-password-1'"
_ <- tcpQuery admin "create role reader"
_ <- tcpQuery admin "use test"
_ <- tcpQuery admin "grant select on users to reader"
_ <- tcpQuery admin "grant reader to bob"
withConnection handle $ \bob -> do
_ <- tcpLogin bob "bob" "bob-password-1"
_ <- tcpQuery bob "use test"
allowed <- tcpQuery bob "select name from users"
asText (at "status" allowed) `shouldBe` "result"
_ <- tcpQuery admin "revoke reader from bob"
tcpClosed bob
it "disconnects every member of a role that was dropped" $
withTcpServer defaultServerConfig $ \_ handle ->
withConnection handle $ \admin -> do
_ <- tcpLogin admin "admin" "s3cret"
_ <- tcpQuery admin "create user bob identified by 'bob-password-1'"
_ <- tcpQuery admin "create role reader"
_ <- tcpQuery admin "grant reader to bob"
withConnection handle $ \bob -> do
_ <- tcpLogin bob "bob" "bob-password-1"
_ <- tcpQuery admin "drop role reader"
tcpClosed bob
it "disconnects an account whose password was reset" $
withTcpServer defaultServerConfig $ \_ handle ->
withConnection handle $ \admin -> do
_ <- tcpLogin admin "admin" "s3cret"
_ <- tcpQuery admin "create user bob identified by 'bob-password-1'"
withConnection handle $ \bob -> do
_ <- tcpLogin bob "bob" "bob-password-1"
_ <- tcpQuery admin "alter user bob identified by 'bob-password-2'"
tcpClosed bob
withConnection handle $ \again -> do
relogin <- tcpLogin again "bob" "bob-password-2"
asText (at "status" relogin) `shouldBe` "ok"
it "disconnects an account that was dropped" $
withTcpServer defaultServerConfig $ \_ handle ->
withConnection handle $ \admin -> do
_ <- tcpLogin admin "admin" "s3cret"
_ <- tcpQuery admin "create user bob identified by 'bob-password-1'"
withConnection handle $ \bob -> do
_ <- tcpLogin bob "bob" "bob-password-1"
_ <- tcpQuery admin "drop user bob"
tcpClosed bob
it "leaves the connections of untouched accounts alone" $
withTcpServer defaultServerConfig $ \_ handle ->
withConnection handle $ \admin -> do
_ <- tcpLogin admin "admin" "s3cret"
_ <- tcpQuery admin "create user bob identified by 'bob-password-1'"
withConnection handle $ \bob -> do
_ <- tcpLogin bob "bob" "bob-password-1"
_ <- tcpQuery admin "create user carol identified by 'carol-password-1'"
_ <- tcpQuery admin "alter user carol identified by 'carol-password-2'"
still <- tcpQuery bob "use test"
asText (at "status" still) `shouldBe` "result"
-- 权限服务夹具:内存后端上的系统库视角
-- | 一个普通账号
@@ -695,6 +757,17 @@ privilegeSpec = describe "server privileges" $ do
authorizeSql service (Ordinary testAccount) "SELECT name FROM users WHERE id > ANY (SELECT id FROM orders)"
`shouldReturn` Left (PrivilegeError "forbidden" "permission denied: SELECT ON orders")
it "lists the accounts a role change touches, members included" $ withPrivileges $ \service root -> do
_ <- runPrivilegeCommand service root testDatabase (CreateRoleCommand "reader")
_ <- runPrivilegeCommand service root testDatabase (CreateRoleCommand "inner")
_ <- runPrivilegeCommand service root testDatabase (GrantRoleCommand "inner" ["reader"])
_ <- runPrivilegeCommand service root testDatabase (GrantRoleCommand "reader" ["alice"])
directly <- affectedAccounts service (RevokeRoleCommand "inner" ["bob"])
directly `shouldMatchList` ["inner", "bob", "reader", "alice"]
dropped <- affectedAccounts service (DropRoleCommand "inner")
dropped `shouldMatchList` ["inner", "reader", "alice"]
affectedAccounts service (CreateRoleCommand "other") `shouldReturn` []
it "keeps the role tables on the real storage" $ withIpcStorage $ do
base <- ipcBackend
service <- newPrivileges base