|
|
@@ -1,7 +1,137 @@
|
|
|
const db = require('../plugin/DataBase/db')
|
|
|
const Redis = require('../plugin/DataBase/Redis')
|
|
|
+const {
|
|
|
+ DEFAULT_PERMISSION_POINTS,
|
|
|
+ DEFAULT_PERMISSION_RESOURCE_RULES,
|
|
|
+ DEFAULT_BASIC_USER_PERMISSION_CODES,
|
|
|
+ LEGACY_ROLE_PERMISSION_MAP
|
|
|
+} = require('./PermissionCatalog')
|
|
|
|
|
|
class AccessControl {
|
|
|
+ constructor() {
|
|
|
+ this.schemaReady = false
|
|
|
+ }
|
|
|
+
|
|
|
+ parseArray(value) {
|
|
|
+ if (Array.isArray(value)) return value
|
|
|
+ if (!value) return []
|
|
|
+ if (typeof value !== 'string') return []
|
|
|
+ try {
|
|
|
+ const parsed = JSON.parse(value)
|
|
|
+ return Array.isArray(parsed) ? parsed : []
|
|
|
+ } catch (_) {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ normalizeCodes(codes) {
|
|
|
+ if (!codes) return []
|
|
|
+ const list = Array.isArray(codes) ? codes : [codes]
|
|
|
+ return [...new Set(list.map(code => String(code || '').trim()).filter(Boolean))]
|
|
|
+ }
|
|
|
+
|
|
|
+ async ensurePermissionSchema() {
|
|
|
+ if (this.schemaReady) return
|
|
|
+
|
|
|
+ await db.query(`
|
|
|
+ CREATE TABLE IF NOT EXISTS permission_points (
|
|
|
+ id INT NOT NULL AUTO_INCREMENT,
|
|
|
+ code VARCHAR(120) NOT NULL,
|
|
|
+ name VARCHAR(120) NOT NULL,
|
|
|
+ category VARCHAR(40) NOT NULL DEFAULT 'action',
|
|
|
+ scope_type VARCHAR(40) NOT NULL DEFAULT 'action',
|
|
|
+ page_route_name VARCHAR(120) DEFAULT NULL,
|
|
|
+ enabled TINYINT NOT NULL DEFAULT 1,
|
|
|
+ remark VARCHAR(255) DEFAULT '',
|
|
|
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
+ PRIMARY KEY (id),
|
|
|
+ UNIQUE KEY uniq_permission_points_code (code)
|
|
|
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
|
+ `)
|
|
|
+
|
|
|
+ await db.query(`
|
|
|
+ CREATE TABLE IF NOT EXISTS user_permission_points (
|
|
|
+ id INT NOT NULL AUTO_INCREMENT,
|
|
|
+ user_uuid VARCHAR(64) NOT NULL,
|
|
|
+ permission_code VARCHAR(120) NOT NULL,
|
|
|
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ PRIMARY KEY (id),
|
|
|
+ UNIQUE KEY uniq_user_permission (user_uuid, permission_code),
|
|
|
+ KEY idx_user_permission_user_uuid (user_uuid),
|
|
|
+ KEY idx_user_permission_code (permission_code)
|
|
|
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
|
+ `)
|
|
|
+
|
|
|
+ await db.query(`
|
|
|
+ CREATE TABLE IF NOT EXISTS user_basic_permission_denials (
|
|
|
+ id INT NOT NULL AUTO_INCREMENT,
|
|
|
+ user_uuid VARCHAR(64) NOT NULL,
|
|
|
+ permission_code VARCHAR(120) NOT NULL,
|
|
|
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ PRIMARY KEY (id),
|
|
|
+ UNIQUE KEY uniq_user_basic_denial (user_uuid, permission_code),
|
|
|
+ KEY idx_user_basic_denial_user_uuid (user_uuid)
|
|
|
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
|
+ `)
|
|
|
+
|
|
|
+ await db.query(`
|
|
|
+ CREATE TABLE IF NOT EXISTS permission_resource_rules (
|
|
|
+ id INT NOT NULL AUTO_INCREMENT,
|
|
|
+ resource_type VARCHAR(40) NOT NULL,
|
|
|
+ resource_key VARCHAR(180) NOT NULL,
|
|
|
+ api_method VARCHAR(16) DEFAULT NULL,
|
|
|
+ api_path VARCHAR(180) DEFAULT NULL,
|
|
|
+ required_codes TEXT NOT NULL,
|
|
|
+ enabled TINYINT NOT NULL DEFAULT 1,
|
|
|
+ remark VARCHAR(255) DEFAULT '',
|
|
|
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
+ PRIMARY KEY (id),
|
|
|
+ UNIQUE KEY uniq_permission_resource (resource_type, resource_key),
|
|
|
+ KEY idx_permission_resource_api (api_method, api_path)
|
|
|
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
|
+ `)
|
|
|
+
|
|
|
+ for (const point of DEFAULT_PERMISSION_POINTS) {
|
|
|
+ await db.query(
|
|
|
+ `INSERT INTO permission_points
|
|
|
+ (code, name, category, scope_type, page_route_name, enabled, remark)
|
|
|
+ VALUES (?, ?, ?, ?, ?, 1, ?)
|
|
|
+ ON DUPLICATE KEY UPDATE
|
|
|
+ name = VALUES(name),
|
|
|
+ category = VALUES(category),
|
|
|
+ scope_type = VALUES(scope_type),
|
|
|
+ page_route_name = VALUES(page_route_name),
|
|
|
+ remark = VALUES(remark)`,
|
|
|
+ [point.code, point.name, point.category, point.scope_type, point.page_route_name || null, point.remark || '']
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const rule of DEFAULT_PERMISSION_RESOURCE_RULES) {
|
|
|
+ const requiredCodes = JSON.stringify(this.normalizeCodes(rule.required_codes))
|
|
|
+ await db.query(
|
|
|
+ `INSERT INTO permission_resource_rules
|
|
|
+ (resource_type, resource_key, api_method, api_path, required_codes, enabled, remark)
|
|
|
+ VALUES (?, ?, ?, ?, ?, 1, ?)
|
|
|
+ ON DUPLICATE KEY UPDATE
|
|
|
+ api_method = VALUES(api_method),
|
|
|
+ api_path = VALUES(api_path),
|
|
|
+ remark = VALUES(remark)`,
|
|
|
+ [
|
|
|
+ rule.resource_type,
|
|
|
+ rule.resource_key,
|
|
|
+ rule.api_method || null,
|
|
|
+ rule.api_path || null,
|
|
|
+ requiredCodes,
|
|
|
+ rule.remark || ''
|
|
|
+ ]
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ this.schemaReady = true
|
|
|
+ }
|
|
|
+
|
|
|
async checkSession(uuid, session) {
|
|
|
return (await Redis.get(`userSession:${uuid}`)) === session
|
|
|
}
|
|
|
@@ -20,7 +150,187 @@ class AccessControl {
|
|
|
const sql = 'SELECT permission FROM users WHERE uuid = ?'
|
|
|
const rows = await db.query(sql, [uuid])
|
|
|
|
|
|
- return rows[0]?.permission || []
|
|
|
+ return this.parseArray(rows?.[0]?.permission)
|
|
|
+ }
|
|
|
+
|
|
|
+ isSuperPermission(permission) {
|
|
|
+ return this.parseArray(permission).includes('admin')
|
|
|
+ }
|
|
|
+
|
|
|
+ async isSuperAdmin(uuid) {
|
|
|
+ const permission = await this.getPermission(uuid)
|
|
|
+ return this.isSuperPermission(permission)
|
|
|
+ }
|
|
|
+
|
|
|
+ async getPermissionPoints() {
|
|
|
+ await this.ensurePermissionSchema()
|
|
|
+ const rows = await db.query(`
|
|
|
+ SELECT id, code, name, category, scope_type, page_route_name, enabled, remark
|
|
|
+ FROM permission_points
|
|
|
+ ORDER BY category, id
|
|
|
+ `)
|
|
|
+ return rows || []
|
|
|
+ }
|
|
|
+
|
|
|
+ async getUserDirectPermissionCodes(uuid) {
|
|
|
+ await this.ensurePermissionSchema()
|
|
|
+ const rows = await db.query(
|
|
|
+ `SELECT permission_code FROM user_permission_points WHERE user_uuid = ? ORDER BY permission_code`,
|
|
|
+ [uuid]
|
|
|
+ )
|
|
|
+ return (rows || []).map(row => row.permission_code)
|
|
|
+ }
|
|
|
+
|
|
|
+ async getUserDeniedBasicPermissionCodes(uuid) {
|
|
|
+ await this.ensurePermissionSchema()
|
|
|
+ const rows = await db.query(
|
|
|
+ `SELECT permission_code FROM user_basic_permission_denials WHERE user_uuid = ? ORDER BY permission_code`,
|
|
|
+ [uuid]
|
|
|
+ )
|
|
|
+ const denied = (rows || []).map(row => row.permission_code)
|
|
|
+ const basicSet = new Set(DEFAULT_BASIC_USER_PERMISSION_CODES)
|
|
|
+ return this.normalizeCodes(denied.filter(code => basicSet.has(code)))
|
|
|
+ }
|
|
|
+
|
|
|
+ getEnabledBasicPermissionCodes(deniedBasicCodes = []) {
|
|
|
+ const deniedSet = new Set(this.normalizeCodes(deniedBasicCodes))
|
|
|
+ return DEFAULT_BASIC_USER_PERMISSION_CODES.filter(code => !deniedSet.has(code))
|
|
|
+ }
|
|
|
+
|
|
|
+ async getUserPermissionCodes(uuid) {
|
|
|
+ await this.ensurePermissionSchema()
|
|
|
+ const legacyRoles = await this.getPermission(uuid)
|
|
|
+ const directCodes = await this.getUserDirectPermissionCodes(uuid)
|
|
|
+ const deniedBasicCodes = await this.getUserDeniedBasicPermissionCodes(uuid)
|
|
|
+ const roleCodes = legacyRoles.flatMap(role => LEGACY_ROLE_PERMISSION_MAP[role] || [])
|
|
|
+
|
|
|
+ return this.normalizeCodes([
|
|
|
+ ...this.getEnabledBasicPermissionCodes(deniedBasicCodes),
|
|
|
+ ...legacyRoles,
|
|
|
+ ...roleCodes,
|
|
|
+ ...directCodes
|
|
|
+ ])
|
|
|
+ }
|
|
|
+
|
|
|
+ async setUserPermissionCodes(uuid, codes) {
|
|
|
+ await this.ensurePermissionSchema()
|
|
|
+ const permissionCodes = this.normalizeCodes(codes)
|
|
|
+ const points = await this.getPermissionPoints()
|
|
|
+ const validCodes = new Set(points.map(point => point.code))
|
|
|
+ const invalidCodes = permissionCodes.filter(code => !validCodes.has(code))
|
|
|
+
|
|
|
+ if (invalidCodes.length > 0)
|
|
|
+ throw new Error(`存在无效权限点:${invalidCodes.join(', ')}`)
|
|
|
+
|
|
|
+ const conn = await db.connect()
|
|
|
+ await conn.beginTransaction()
|
|
|
+ try {
|
|
|
+ await conn.execute(`DELETE FROM user_permission_points WHERE user_uuid = ?`, [uuid])
|
|
|
+ for (const code of permissionCodes) {
|
|
|
+ await conn.execute(
|
|
|
+ `INSERT INTO user_permission_points (user_uuid, permission_code) VALUES (?, ?)`,
|
|
|
+ [uuid, code]
|
|
|
+ )
|
|
|
+ }
|
|
|
+ await conn.commit()
|
|
|
+ } catch (error) {
|
|
|
+ try { await conn.rollback() } catch (_) { }
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async setUserDeniedBasicPermissionCodes(uuid, codes) {
|
|
|
+ await this.ensurePermissionSchema()
|
|
|
+ const basicSet = new Set(DEFAULT_BASIC_USER_PERMISSION_CODES)
|
|
|
+ const deniedCodes = this.normalizeCodes(codes).filter(code => basicSet.has(code))
|
|
|
+ const invalidCodes = this.normalizeCodes(codes).filter(code => !basicSet.has(code))
|
|
|
+
|
|
|
+ if (invalidCodes.length > 0)
|
|
|
+ throw new Error(`仅可关闭基础权限:${invalidCodes.join(', ')}`)
|
|
|
+
|
|
|
+ const conn = await db.connect()
|
|
|
+ await conn.beginTransaction()
|
|
|
+ try {
|
|
|
+ await conn.execute(`DELETE FROM user_basic_permission_denials WHERE user_uuid = ?`, [uuid])
|
|
|
+ for (const code of deniedCodes) {
|
|
|
+ await conn.execute(
|
|
|
+ `INSERT INTO user_basic_permission_denials (user_uuid, permission_code) VALUES (?, ?)`,
|
|
|
+ [uuid, code]
|
|
|
+ )
|
|
|
+ }
|
|
|
+ await conn.commit()
|
|
|
+ } catch (error) {
|
|
|
+ try { await conn.rollback() } catch (_) { }
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async getResourceRules() {
|
|
|
+ await this.ensurePermissionSchema()
|
|
|
+ const rows = await db.query(`
|
|
|
+ SELECT id, resource_type, resource_key, api_method, api_path, required_codes, enabled, remark
|
|
|
+ FROM permission_resource_rules
|
|
|
+ ORDER BY FIELD(resource_type, 'page', 'action', 'api'), id
|
|
|
+ `)
|
|
|
+
|
|
|
+ return (rows || []).map(row => ({
|
|
|
+ ...row,
|
|
|
+ required_codes: this.parseArray(row.required_codes)
|
|
|
+ }))
|
|
|
+ }
|
|
|
+
|
|
|
+ async getResourceRequiredCodes({ resourceType, resourceKey, method, path }) {
|
|
|
+ await this.ensurePermissionSchema()
|
|
|
+ let rows = []
|
|
|
+
|
|
|
+ if (resourceType && resourceKey) {
|
|
|
+ rows = await db.query(
|
|
|
+ `SELECT required_codes
|
|
|
+ FROM permission_resource_rules
|
|
|
+ WHERE resource_type = ? AND resource_key = ? AND enabled = 1
|
|
|
+ LIMIT 1`,
|
|
|
+ [resourceType, resourceKey]
|
|
|
+ )
|
|
|
+ } else if (method && path) {
|
|
|
+ rows = await db.query(
|
|
|
+ `SELECT required_codes
|
|
|
+ FROM permission_resource_rules
|
|
|
+ WHERE resource_type = 'api' AND api_method = ? AND api_path = ? AND enabled = 1
|
|
|
+ LIMIT 1`,
|
|
|
+ [String(method).toUpperCase(), path]
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.parseArray(rows?.[0]?.required_codes)
|
|
|
+ }
|
|
|
+
|
|
|
+ async updateResourceRule({ id, required_codes, enabled }) {
|
|
|
+ await this.ensurePermissionSchema()
|
|
|
+ const requiredCodes = this.normalizeCodes(required_codes)
|
|
|
+ const points = await this.getPermissionPoints()
|
|
|
+ const validCodes = new Set(points.map(point => point.code))
|
|
|
+ const invalidCodes = requiredCodes.filter(code => !validCodes.has(code))
|
|
|
+
|
|
|
+ if (invalidCodes.length > 0)
|
|
|
+ throw new Error(`存在无效权限点:${invalidCodes.join(', ')}`)
|
|
|
+
|
|
|
+ const rows = await db.query(
|
|
|
+ `UPDATE permission_resource_rules
|
|
|
+ SET required_codes = ?, enabled = ?
|
|
|
+ WHERE id = ?`,
|
|
|
+ [JSON.stringify(requiredCodes), Number(enabled) === 0 ? 0 : 1, id]
|
|
|
+ )
|
|
|
+
|
|
|
+ return rows?.affectedRows === 1
|
|
|
+ }
|
|
|
+
|
|
|
+ async canAccess(uuid, requiredCodes) {
|
|
|
+ const codes = this.normalizeCodes(requiredCodes)
|
|
|
+ if (codes.length === 0) return true
|
|
|
+ if (await this.isSuperAdmin(uuid)) return true
|
|
|
+
|
|
|
+ const userCodes = await this.getUserPermissionCodes(uuid)
|
|
|
+ return codes.some(code => userCodes.includes(code))
|
|
|
}
|
|
|
|
|
|
async checkJwAccount(uuid, username) {
|