AccessControl.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. const db = require('../plugin/DataBase/db')
  2. const Redis = require('../plugin/DataBase/Redis')
  3. const {
  4. DEFAULT_PERMISSION_POINTS,
  5. DEFAULT_PERMISSION_RESOURCE_RULES,
  6. DEFAULT_BASIC_USER_PERMISSION_CODES,
  7. LEGACY_ROLE_PERMISSION_MAP
  8. } = require('./PermissionCatalog')
  9. class AccessControl {
  10. constructor() {
  11. this.schemaReady = false
  12. }
  13. parseArray(value) {
  14. if (Array.isArray(value)) return value
  15. if (!value) return []
  16. if (typeof value !== 'string') return []
  17. try {
  18. const parsed = JSON.parse(value)
  19. return Array.isArray(parsed) ? parsed : []
  20. } catch (_) {
  21. return []
  22. }
  23. }
  24. normalizeCodes(codes) {
  25. if (!codes) return []
  26. const list = Array.isArray(codes) ? codes : [codes]
  27. return [...new Set(list.map(code => String(code || '').trim()).filter(Boolean))]
  28. }
  29. async ensurePermissionSchema() {
  30. if (this.schemaReady) return
  31. await db.query(`
  32. CREATE TABLE IF NOT EXISTS permission_points (
  33. id INT NOT NULL AUTO_INCREMENT,
  34. code VARCHAR(120) NOT NULL,
  35. name VARCHAR(120) NOT NULL,
  36. category VARCHAR(40) NOT NULL DEFAULT 'action',
  37. scope_type VARCHAR(40) NOT NULL DEFAULT 'action',
  38. page_route_name VARCHAR(120) DEFAULT NULL,
  39. enabled TINYINT NOT NULL DEFAULT 1,
  40. remark VARCHAR(255) DEFAULT '',
  41. created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  42. updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  43. PRIMARY KEY (id),
  44. UNIQUE KEY uniq_permission_points_code (code)
  45. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  46. `)
  47. await db.query(`
  48. CREATE TABLE IF NOT EXISTS user_permission_points (
  49. id INT NOT NULL AUTO_INCREMENT,
  50. user_uuid VARCHAR(64) NOT NULL,
  51. permission_code VARCHAR(120) NOT NULL,
  52. created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  53. PRIMARY KEY (id),
  54. UNIQUE KEY uniq_user_permission (user_uuid, permission_code),
  55. KEY idx_user_permission_user_uuid (user_uuid),
  56. KEY idx_user_permission_code (permission_code)
  57. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  58. `)
  59. await db.query(`
  60. CREATE TABLE IF NOT EXISTS user_basic_permission_denials (
  61. id INT NOT NULL AUTO_INCREMENT,
  62. user_uuid VARCHAR(64) NOT NULL,
  63. permission_code VARCHAR(120) NOT NULL,
  64. created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  65. PRIMARY KEY (id),
  66. UNIQUE KEY uniq_user_basic_denial (user_uuid, permission_code),
  67. KEY idx_user_basic_denial_user_uuid (user_uuid)
  68. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  69. `)
  70. await db.query(`
  71. CREATE TABLE IF NOT EXISTS permission_resource_rules (
  72. id INT NOT NULL AUTO_INCREMENT,
  73. resource_type VARCHAR(40) NOT NULL,
  74. resource_key VARCHAR(180) NOT NULL,
  75. api_method VARCHAR(16) DEFAULT NULL,
  76. api_path VARCHAR(180) DEFAULT NULL,
  77. required_codes TEXT NOT NULL,
  78. enabled TINYINT NOT NULL DEFAULT 1,
  79. remark VARCHAR(255) DEFAULT '',
  80. created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  81. updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  82. PRIMARY KEY (id),
  83. UNIQUE KEY uniq_permission_resource (resource_type, resource_key),
  84. KEY idx_permission_resource_api (api_method, api_path)
  85. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  86. `)
  87. for (const point of DEFAULT_PERMISSION_POINTS) {
  88. await db.query(
  89. `INSERT INTO permission_points
  90. (code, name, category, scope_type, page_route_name, enabled, remark)
  91. VALUES (?, ?, ?, ?, ?, 1, ?)
  92. ON DUPLICATE KEY UPDATE
  93. name = VALUES(name),
  94. category = VALUES(category),
  95. scope_type = VALUES(scope_type),
  96. page_route_name = VALUES(page_route_name),
  97. remark = VALUES(remark)`,
  98. [point.code, point.name, point.category, point.scope_type, point.page_route_name || null, point.remark || '']
  99. )
  100. }
  101. for (const rule of DEFAULT_PERMISSION_RESOURCE_RULES) {
  102. const requiredCodes = JSON.stringify(this.normalizeCodes(rule.required_codes))
  103. await db.query(
  104. `INSERT INTO permission_resource_rules
  105. (resource_type, resource_key, api_method, api_path, required_codes, enabled, remark)
  106. VALUES (?, ?, ?, ?, ?, 1, ?)
  107. ON DUPLICATE KEY UPDATE
  108. api_method = VALUES(api_method),
  109. api_path = VALUES(api_path),
  110. remark = VALUES(remark)`,
  111. [
  112. rule.resource_type,
  113. rule.resource_key,
  114. rule.api_method || null,
  115. rule.api_path || null,
  116. requiredCodes,
  117. rule.remark || ''
  118. ]
  119. )
  120. }
  121. this.schemaReady = true
  122. }
  123. async checkSession(uuid, session) {
  124. return (await Redis.get(`userSession:${uuid}`)) === session
  125. }
  126. async isBanned(uuid) {
  127. const sql = 'SELECT COALESCE(is_banned, 0) AS is_banned FROM users WHERE uuid = ?'
  128. const rows = await db.query(sql, [uuid])
  129. return Number(rows[0]?.is_banned) === 1
  130. }
  131. async invalidateSession(uuid) {
  132. await Redis.del(`userSession:${uuid}`)
  133. }
  134. async getPermission(uuid) {
  135. const sql = 'SELECT permission FROM users WHERE uuid = ?'
  136. const rows = await db.query(sql, [uuid])
  137. return this.parseArray(rows?.[0]?.permission)
  138. }
  139. isSuperPermission(permission) {
  140. return this.parseArray(permission).includes('admin')
  141. }
  142. async isSuperAdmin(uuid) {
  143. const permission = await this.getPermission(uuid)
  144. return this.isSuperPermission(permission)
  145. }
  146. async getPermissionPoints() {
  147. await this.ensurePermissionSchema()
  148. const rows = await db.query(`
  149. SELECT id, code, name, category, scope_type, page_route_name, enabled, remark
  150. FROM permission_points
  151. ORDER BY category, id
  152. `)
  153. return rows || []
  154. }
  155. async getUserDirectPermissionCodes(uuid) {
  156. await this.ensurePermissionSchema()
  157. const rows = await db.query(
  158. `SELECT permission_code FROM user_permission_points WHERE user_uuid = ? ORDER BY permission_code`,
  159. [uuid]
  160. )
  161. return (rows || []).map(row => row.permission_code)
  162. }
  163. async getUserDeniedBasicPermissionCodes(uuid) {
  164. await this.ensurePermissionSchema()
  165. const rows = await db.query(
  166. `SELECT permission_code FROM user_basic_permission_denials WHERE user_uuid = ? ORDER BY permission_code`,
  167. [uuid]
  168. )
  169. const denied = (rows || []).map(row => row.permission_code)
  170. const basicSet = new Set(DEFAULT_BASIC_USER_PERMISSION_CODES)
  171. return this.normalizeCodes(denied.filter(code => basicSet.has(code)))
  172. }
  173. getEnabledBasicPermissionCodes(deniedBasicCodes = []) {
  174. const deniedSet = new Set(this.normalizeCodes(deniedBasicCodes))
  175. return DEFAULT_BASIC_USER_PERMISSION_CODES.filter(code => !deniedSet.has(code))
  176. }
  177. async getUserPermissionCodes(uuid) {
  178. await this.ensurePermissionSchema()
  179. const legacyRoles = await this.getPermission(uuid)
  180. const directCodes = await this.getUserDirectPermissionCodes(uuid)
  181. const deniedBasicCodes = await this.getUserDeniedBasicPermissionCodes(uuid)
  182. const roleCodes = legacyRoles.flatMap(role => LEGACY_ROLE_PERMISSION_MAP[role] || [])
  183. return this.normalizeCodes([
  184. ...this.getEnabledBasicPermissionCodes(deniedBasicCodes),
  185. ...legacyRoles,
  186. ...roleCodes,
  187. ...directCodes
  188. ])
  189. }
  190. async setUserPermissionCodes(uuid, codes) {
  191. await this.ensurePermissionSchema()
  192. const permissionCodes = this.normalizeCodes(codes)
  193. const points = await this.getPermissionPoints()
  194. const validCodes = new Set(points.map(point => point.code))
  195. const invalidCodes = permissionCodes.filter(code => !validCodes.has(code))
  196. if (invalidCodes.length > 0)
  197. throw new Error(`存在无效权限点:${invalidCodes.join(', ')}`)
  198. const conn = await db.connect()
  199. await conn.beginTransaction()
  200. try {
  201. await conn.execute(`DELETE FROM user_permission_points WHERE user_uuid = ?`, [uuid])
  202. for (const code of permissionCodes) {
  203. await conn.execute(
  204. `INSERT INTO user_permission_points (user_uuid, permission_code) VALUES (?, ?)`,
  205. [uuid, code]
  206. )
  207. }
  208. await conn.commit()
  209. } catch (error) {
  210. try { await conn.rollback() } catch (_) { }
  211. throw error
  212. }
  213. }
  214. async setUserDeniedBasicPermissionCodes(uuid, codes) {
  215. await this.ensurePermissionSchema()
  216. const basicSet = new Set(DEFAULT_BASIC_USER_PERMISSION_CODES)
  217. const deniedCodes = this.normalizeCodes(codes).filter(code => basicSet.has(code))
  218. const invalidCodes = this.normalizeCodes(codes).filter(code => !basicSet.has(code))
  219. if (invalidCodes.length > 0)
  220. throw new Error(`仅可关闭基础权限:${invalidCodes.join(', ')}`)
  221. const conn = await db.connect()
  222. await conn.beginTransaction()
  223. try {
  224. await conn.execute(`DELETE FROM user_basic_permission_denials WHERE user_uuid = ?`, [uuid])
  225. for (const code of deniedCodes) {
  226. await conn.execute(
  227. `INSERT INTO user_basic_permission_denials (user_uuid, permission_code) VALUES (?, ?)`,
  228. [uuid, code]
  229. )
  230. }
  231. await conn.commit()
  232. } catch (error) {
  233. try { await conn.rollback() } catch (_) { }
  234. throw error
  235. }
  236. }
  237. async getResourceRules() {
  238. await this.ensurePermissionSchema()
  239. const rows = await db.query(`
  240. SELECT id, resource_type, resource_key, api_method, api_path, required_codes, enabled, remark
  241. FROM permission_resource_rules
  242. ORDER BY FIELD(resource_type, 'page', 'action', 'api'), id
  243. `)
  244. return (rows || []).map(row => ({
  245. ...row,
  246. required_codes: this.parseArray(row.required_codes)
  247. }))
  248. }
  249. async getResourceRequiredCodes({ resourceType, resourceKey, method, path }) {
  250. await this.ensurePermissionSchema()
  251. let rows = []
  252. if (resourceType && resourceKey) {
  253. rows = await db.query(
  254. `SELECT required_codes
  255. FROM permission_resource_rules
  256. WHERE resource_type = ? AND resource_key = ? AND enabled = 1
  257. LIMIT 1`,
  258. [resourceType, resourceKey]
  259. )
  260. } else if (method && path) {
  261. rows = await db.query(
  262. `SELECT required_codes
  263. FROM permission_resource_rules
  264. WHERE resource_type = 'api' AND api_method = ? AND api_path = ? AND enabled = 1
  265. LIMIT 1`,
  266. [String(method).toUpperCase(), path]
  267. )
  268. }
  269. return this.parseArray(rows?.[0]?.required_codes)
  270. }
  271. async updateResourceRule({ id, required_codes, enabled }) {
  272. await this.ensurePermissionSchema()
  273. const requiredCodes = this.normalizeCodes(required_codes)
  274. const points = await this.getPermissionPoints()
  275. const validCodes = new Set(points.map(point => point.code))
  276. const invalidCodes = requiredCodes.filter(code => !validCodes.has(code))
  277. if (invalidCodes.length > 0)
  278. throw new Error(`存在无效权限点:${invalidCodes.join(', ')}`)
  279. const rows = await db.query(
  280. `UPDATE permission_resource_rules
  281. SET required_codes = ?, enabled = ?
  282. WHERE id = ?`,
  283. [JSON.stringify(requiredCodes), Number(enabled) === 0 ? 0 : 1, id]
  284. )
  285. return rows?.affectedRows === 1
  286. }
  287. async canAccess(uuid, requiredCodes) {
  288. const codes = this.normalizeCodes(requiredCodes)
  289. if (codes.length === 0) return true
  290. if (await this.isSuperAdmin(uuid)) return true
  291. const userCodes = await this.getUserPermissionCodes(uuid)
  292. return codes.some(code => userCodes.includes(code))
  293. }
  294. async checkJwAccount(uuid, username) {
  295. const sql = 'SELECT password FROM jw_account WHERE create_user = ? AND state = 1 AND username = ?'
  296. const rows = await db.query(sql, [uuid, username]);
  297. if (!rows || rows.length !== 1 || !rows[0].password)
  298. return false
  299. return rows[0]?.password
  300. }
  301. }
  302. module.exports = new AccessControl();