From 29529444a1586f53cb40407167610a7ff96bb19e Mon Sep 17 00:00:00 2001 From: LIm1tZz7 <36764786+TheBadRoger@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:16:26 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E8=A1=A8=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E4=B8=8E=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chusql-core/storage/src/catalog.rs | 17 ++++++ chusql-core/storage/src/protocol.rs | 13 +++++ chusql-core/storage/src/runtime.rs | 67 +++++++++++++++++++--- chusql-core/storage/tests/runtime_test.rs | 53 +++++++++++++++++ chusql-server/src/ChuSQL/Server/Backend.hs | 22 ++++--- chusql-server/test/Spec.hs | 19 ++++++ docs/architecture.md | 2 +- 7 files changed, 178 insertions(+), 15 deletions(-) diff --git a/chusql-core/storage/src/catalog.rs b/chusql-core/storage/src/catalog.rs index bb38dfd..6c9cacb 100644 --- a/chusql-core/storage/src/catalog.rs +++ b/chusql-core/storage/src/catalog.rs @@ -202,6 +202,23 @@ impl Catalog { Ok(()) } + /// 表改名到指定名字 + pub fn rename_table(&mut self, from: &str, to: &str) -> io::Result { + let Some(entry) = self.tables.remove(from) else { + return Ok(false); + }; + if self.tables.contains_key(to) { + self.tables.insert(from.to_string(), entry); + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("table already exists: {}", to), + )); + } + self.seen.remove(from); + self.tables.insert(to.to_string(), entry); + Ok(true) + } + /// ALTER:整列定义换成新的,并清掉二级索引与统计 pub fn set_columns(&mut self, table: &str, columns: Vec) -> io::Result<()> { let entry = self diff --git a/chusql-core/storage/src/protocol.rs b/chusql-core/storage/src/protocol.rs index 39276f5..0910dfc 100644 --- a/chusql-core/storage/src/protocol.rs +++ b/chusql-core/storage/src/protocol.rs @@ -229,6 +229,19 @@ pub fn reserved_table(table: &str) -> bool { table.to_ascii_lowercase().starts_with("__system_") } +/// 服务自己的角色与授权表名 +pub const PRIVILEGE_TABLES: [&str; 3] = ["__system_roles", "__system_grants", "__system_members"]; + +/// 是不是服务自己经请求通道读写的系统表 +pub fn internal_table(table: &str) -> bool { + PRIVILEGE_TABLES.contains(&table.to_ascii_lowercase().as_str()) +} + +/// 是不是请求通道要拒绝的系统表 +pub fn blocked_table(table: &str) -> bool { + reserved_table(table) && !internal_table(table) +} + /// 没写 column 时按 `id` 算 fn default_id_column() -> String { "id".to_string() diff --git a/chusql-core/storage/src/runtime.rs b/chusql-core/storage/src/runtime.rs index 497dbfc..a28591e 100644 --- a/chusql-core/storage/src/runtime.rs +++ b/chusql-core/storage/src/runtime.rs @@ -8,8 +8,8 @@ use crate::config::{self, Config, Loaded}; use crate::heap::{scalar_int, HeapTable}; use crate::log; use crate::protocol::{ - reserved_table, Account, ColumnStatWire, IndexWire, Request, Response, Row, SchemaColumn, StorageOp, - TableSchemaWire, USERS_TABLE, + blocked_table, reserved_table, Account, ColumnStatWire, IndexWire, Request, Response, Row, + SchemaColumn, StorageOp, TableSchemaWire, USERS_TABLE, }; use crate::wal::{committed_ops, Wal, WalOp}; use crate::{log_debug, log_error, log_info, log_warn}; @@ -386,7 +386,7 @@ impl Server { | StorageOp::Delete { table, .. } | StorageOp::Replace { table, .. } => table, }; - if reserved_table(table) { + if blocked_table(table) { return Err(std::io::Error::other("reserved system table")); } if table.is_empty() || !table.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { @@ -604,6 +604,7 @@ impl Server { /// 老数据迁移:类型名规范、缺列补 null fn migrate_data(&self) -> std::io::Result<()> { + self.migrate_table_names()?; let names: Vec = self .catalog .lock() @@ -649,6 +650,51 @@ impl Server { Ok(()) } + /// 老目录里的角色与授权表改到内部前缀 + fn migrate_table_names(&self) -> std::io::Result<()> { + for (from, to) in LEGACY_TABLE_RENAMES { + let indexes = { + let c = self + .catalog + .lock() + .map_err(|_| std::io::Error::other("catalog lock poisoned"))?; + match c.describe(from) { + Some(schema) if c.describe(to).is_none() => { + schema.indexes.iter().map(|i| i.column.clone()).collect::>() + } + _ => continue, + } + }; + self.rename_table_files(from, to, &indexes); + { + let mut c = self + .catalog + .lock() + .map_err(|_| std::io::Error::other("catalog lock poisoned"))?; + if !c.rename_table(from, to)? { + continue; + } + c.save(self.catalog_path())?; + } + if let Ok(mut tables) = self.tables.lock() { + tables.remove(from); + } + log_info!(core, "renamed system table {} to {}", from, to); + } + Ok(()) + } + + /// 把一张表的数据文件与索引文件改名 + fn rename_table_files(&self, from: &str, to: &str, indexes: &[String]) { + if !self.table_path(from).exists() { + return; + } + let _ = std::fs::rename(self.table_path(from), self.table_path(to)); + for column in std::iter::once("id").chain(indexes.iter().map(String::as_str)) { + let _ = std::fs::rename(self.index_path(from, column), self.index_path(to, column)); + } + } + /// 建索引前的检查:内置列、未知表列、重复索引 fn check_create_index(&self, table: &str, column: &str) -> std::io::Result<()> { if column == "id" { @@ -963,7 +1009,7 @@ impl Server { return Response::Error { message: "invalid column name".into() }; } if let Some(table) = req.table() { - if reserved_table(table) { + if blocked_table(table) { return Response::Error { message: "reserved system table".into() }; } if table.is_empty() || !table.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { @@ -1192,7 +1238,7 @@ impl Server { let schemas: Vec = c .all_tables() .into_iter() - .filter(|(name, _)| !reserved_table(name)) + .filter(|(name, _)| !blocked_table(name)) .map(|(name, ts)| TableSchemaWire { table: name.to_string(), columns: ts.columns.clone(), @@ -1364,10 +1410,10 @@ impl Server { } } - /// 只列出数据字典中的业务表 + /// 列出业务表与授权服务用的三张系统表 fn list_tables(&self) -> std::io::Result> { let c = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?; - Ok(c.table_names().into_iter().filter(|t| !reserved_table(t)).collect()) + Ok(c.table_names().into_iter().filter(|t| !blocked_table(t)).collect()) } /// 查一张表的 schema @@ -1567,6 +1613,13 @@ impl Drop for Server { /// 别的库一律要显式建(没有默认工作库)。 const SYSTEM_DATABASE: &str = "system"; +/// 老目录里角色与授权表的旧名对照 +const LEGACY_TABLE_RENAMES: [(&str, &str); 3] = [ + ("sys_roles", "__system_roles"), + ("sys_grants", "__system_grants"), + ("sys_members", "__system_members"), +]; + /// 保留库:不能建、不能删。 fn reserved_database(name: &str) -> bool { name == SYSTEM_DATABASE diff --git a/chusql-core/storage/tests/runtime_test.rs b/chusql-core/storage/tests/runtime_test.rs index 50e6635..0c1eb23 100644 --- a/chusql-core/storage/tests/runtime_test.rs +++ b/chusql-core/storage/tests/runtime_test.rs @@ -1686,6 +1686,59 @@ fn legacy_catalog_and_partial_rows_are_migrated() -> Result<(), Box Result<(), Box> { + let (server, data) = start_server(); + let mut c = connect()?; + request_ok(&mut c, serde_json::json!({"method":"create_table","database":"system","table":"sys_roles","columns":[{"name":"id","ty":"int"},{"name":"name","ty":"str"}]}))?; + request_ok(&mut c, serde_json::json!({"method":"insert","database":"system","table":"sys_roles","row":{"id":1,"name":"admin"}}))?; + drop(c); + drop(server); + + let system_dir = data.path().join("system"); + assert!(system_dir.join("sys_roles.db").exists(), "老数据文件要先生成"); + + let _restarted = start_server_in(data.path()); + let mut c = connect()?; + + let described: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"describe_table","database":"system","table":"__system_roles"}"#))?; + assert_eq!(described["status"], "schema", "{described}"); + let rows: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"scan","database":"system","table":"__system_roles"}"#))?; + assert_eq!(rows["rows"], serde_json::json!([{"id":1,"name":"admin"}]), "行要跟着改名走"); + let old: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"describe_table","database":"system","table":"sys_roles"}"#))?; + assert_eq!(old["status"], "error", "旧名不该还在:{old}"); + assert!(system_dir.join("__system_roles.db").exists(), "数据文件要改名"); + assert!(!system_dir.join("sys_roles.db").exists(), "老数据文件要改掉"); + Ok(()) +} + +/// 角色与授权表能被请求通道读到,账号表不能 +#[test] +fn privilege_tables_are_reachable_but_account_table_is_not() -> Result<(), Box> { + let (_server, _data) = start_server(); + let mut c = connect()?; + request_ok(&mut c, serde_json::json!({"method":"create_table","database":"system","table":"__system_roles","columns":[{"name":"id","ty":"int"},{"name":"name","ty":"str"}]}))?; + let scanned: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"scan","database":"system","table":"__system_roles"}"#))?; + assert_eq!(scanned["status"], "rows", "{scanned}"); + let denied: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"scan","database":"system","table":"__system_users"}"#))?; + assert_eq!(denied["status"], "error", "{denied}"); + assert!( + denied["message"].as_str().unwrap_or("").contains("reserved system table"), + "{denied}" + ); + let listed: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"list_tables","database":"system"}"#))?; + let tables: Vec<&str> = listed["tables"] + .as_array() + .ok_or("missing tables")? + .iter() + .map(|one| one.as_str().unwrap_or("")) + .collect(); + assert!(tables.contains(&"__system_roles"), "授权表要在字典里: {listed}"); + assert!(!tables.contains(&"__system_users"), "账号表不能露面: {listed}"); + Ok(()) +} + /// 账号表落盘类型与索引,登录盖时间戳 #[test] fn account_table_uses_typed_columns_and_stamps_login() -> Result<(), Box> { diff --git a/chusql-server/src/ChuSQL/Server/Backend.hs b/chusql-server/src/ChuSQL/Server/Backend.hs index 5624cd2..a9ba9ab 100644 --- a/chusql-server/src/ChuSQL/Server/Backend.hs +++ b/chusql-server/src/ChuSQL/Server/Backend.hs @@ -188,9 +188,9 @@ memoryBackendWith trusted name ref = } where -- | 按视角过滤库里的表 - scoped = if trusted then filter (not . reserved . fst) else visible + scoped = if trusted then filter (not . blocked . fst) else visible -- | 该视角下要隐藏的表 - hidden = if trusted then reserved else internalTable + hidden = if trusted then blocked else internalTable -- | 取结果列名,空结果回落到解析 colsOf db stmt rows = case rows of [] -> columnsFromStatement db stmt @@ -352,15 +352,15 @@ systemDatabaseName = "system" usersTable :: String usersTable = "__system_users" --- | 角色与授权表的名字,不带内部前缀 +-- | 角色与授权表的名字,在系统库里但服务自己要读写 rolesTable :: String -rolesTable = "sys_roles" +rolesTable = "__system_roles" grantsTable :: String -grantsTable = "sys_grants" +grantsTable = "__system_grants" membersTable :: String -membersTable = "sys_members" +membersTable = "__system_members" internalNames :: [String] internalNames = [rolesTable, grantsTable, membersTable] @@ -369,9 +369,17 @@ internalNames = [rolesTable, grantsTable, membersTable] reserved :: String -> Bool reserved = T.isPrefixOf "__system_" . T.toLower . T.pack +-- | 是不是服务自己要经请求通道读写的表 +privilegeTable :: String -> Bool +privilegeTable name = T.unpack (T.toLower (T.pack name)) `elem` internalNames + -- | 是不是服务自己的内部表 internalTable :: String -> Bool -internalTable name = reserved name || T.unpack (T.toLower (T.pack name)) `elem` internalNames +internalTable name = reserved name || privilegeTable name + +-- | 外部语句能不能碰这张表 +blocked :: String -> Bool +blocked name = reserved name && not (privilegeTable name) -- | 去掉内部表 visible :: Database -> Database diff --git a/chusql-server/test/Spec.hs b/chusql-server/test/Spec.hs index 85a05f0..ddc80e0 100644 --- a/chusql-server/test/Spec.hs +++ b/chusql-server/test/Spec.hs @@ -553,6 +553,25 @@ privilegeSpec = describe "server privileges" $ do Left err -> expectationFailure err Right stmt -> authorize service root testDatabase stmt `shouldReturn` Right () + it "keeps the role tables on the real storage" $ withIpcStorage $ do + base <- ipcBackend + service <- newPrivileges base + created <- runPrivilegeCommand service (Root testRootName) testDatabase (CreateRoleCommand "reader") + created `shouldBe` Right () + views <- listRoleViews service + fmap (map roleName) views `shouldBe` Right ["reader"] + -- 改名后真存储上仍要读得到角色表 + let system = beWithDatabase base "system" + stored <- beStatement system "SELECT * FROM __system_roles" + case stored of + Left err -> expectationFailure ("select __system_roles: " ++ err) + Right result -> concatMap (map snd) (srRows result) `shouldContain` [VStr "reader"] + -- 账号表不给语句通道看到 + denied <- beStatement system "SELECT * FROM __system_users" + case denied of + Left _ -> pure () + Right result -> expectationFailure ("account table must stay hidden: " ++ show (srRows result)) + -- 真存储夹具:一个用例一份数据目录,跑完关链路删干净 -- | 本次用例的数据目录(用例串行跑,固定名够用) diff --git a/docs/architecture.md b/docs/architecture.md index 964e4d8..872c95f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -53,6 +53,6 @@ chusql-server WAL 负责崩溃恢复:每条记录带自增 LSN,一组写操作与一条提交标记一次落盘;进程重启时按标记分组重放已提交的记录、丢弃没有标记的整组,重放前逐条做幂等检查,恢复结束把最大 LSN 写进 `wal.checkpoint` 并清空 WAL。`SystemStatus` 应答里的 `last_lsn` 报告该检查点 LSN。建表、删表、建索引、删索引与数据改动走同一条 WAL,恢复时一并重放。 -系统数据库及部分管理操作仅允许管理员访问。 +系统数据库及部分管理操作仅允许管理员访问。系统表统一用 `__system_` 前缀:账号表 `__system_users` 对语句通道隐藏,角色、授权、成员三张表(`__system_roles` / `__system_grants` / `__system_members`)只在 `system` 库里可见,权限服务自身也通过 SQL 读写它们;存储启动时把旧名 `sys_roles` / `sys_grants` / `sys_members` 连同数据与索引文件一起迁移过来。 服务监听地址、端口和请求限制可在 `chusql.toml` 的 `[server]` 分区中配置。