mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-11 01:50:46 +08:00
change log in
This commit is contained in:
parent
9cf026f605
commit
8ca42dd973
211 changed files with 48417 additions and 109 deletions
258
uds-auth/lib/roles.js
Normal file
258
uds-auth/lib/roles.js
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
/**
|
||||
* uds-auth 角色存储 + 权限管理
|
||||
*
|
||||
* 角色:
|
||||
* super_admin 所有权限 + 用户管理
|
||||
* admin 设置权限 + 仅看自己会话
|
||||
* user 仅看自己会话,无设置
|
||||
*
|
||||
* 持久化: roles.json (单实例文件) + MemoryStore 同步
|
||||
*/
|
||||
import { createHash, randomBytes } from 'node:crypto'
|
||||
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
|
||||
export const ROLES = {
|
||||
SUPER_ADMIN: 'super_admin',
|
||||
ADMIN: 'admin',
|
||||
USER: 'user',
|
||||
FALLBACK_ADMIN: 'fallback_admin', // 特殊,UAC 挂了时用,等同 super_admin 权限
|
||||
}
|
||||
|
||||
export const ROLE_LABELS = {
|
||||
super_admin: '超级管理员',
|
||||
admin: '管理员',
|
||||
user: '普通用户',
|
||||
fallback_admin: '兜底管理员',
|
||||
}
|
||||
|
||||
/** 计算角色权限 (纯函数) */
|
||||
export function computePermissions(role) {
|
||||
switch (role) {
|
||||
case ROLES.SUPER_ADMIN:
|
||||
return { canManageUsers: true, canAccessSettings: true, canViewAllSessions: true }
|
||||
case ROLES.FALLBACK_ADMIN:
|
||||
return { canManageUsers: true, canAccessSettings: true, canViewAllSessions: true }
|
||||
case ROLES.ADMIN:
|
||||
return { canManageUsers: false, canAccessSettings: true, canViewAllSessions: false }
|
||||
default: // user / undefined
|
||||
return { canManageUsers: false, canAccessSettings: false, canViewAllSessions: false }
|
||||
}
|
||||
}
|
||||
|
||||
function hashPassword(password) {
|
||||
return createHash('sha256').update(password).digest('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* RolesStore — 角色存储
|
||||
* 内存 Map + 可选 JSON 文件持久化
|
||||
*
|
||||
* fallback admin: 默认不启用,需在 DSH 设置里配置密码哈希
|
||||
* 登录路径: POST /uds-auth/admin-login { username, password }
|
||||
*/
|
||||
export class RolesStore {
|
||||
constructor(options = {}) {
|
||||
this._roles = new Map() // empNo → role
|
||||
this._firstBootLock = Promise.resolve()
|
||||
this._rolesFile = options.rolesFile ? resolve(options.rolesFile) : null
|
||||
this._fallbackPasswordHash = null // SHA-256 hex,null = 未启用
|
||||
this._fallbackRateLimit = new Map() // ip → { count, resetAt }
|
||||
this._dirty = false
|
||||
this._saveTimer = null
|
||||
}
|
||||
|
||||
// === 持久化 ===
|
||||
|
||||
async init() {
|
||||
if (this._rolesFile) {
|
||||
try {
|
||||
const raw = await readFile(this._rolesFile, 'utf-8')
|
||||
const data = JSON.parse(raw)
|
||||
for (const [empNo, role] of Object.entries(data.roles || {})) {
|
||||
this._roles.set(empNo, role)
|
||||
}
|
||||
if (data.fallbackPasswordHash) {
|
||||
this._fallbackPasswordHash = data.fallbackPasswordHash
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
// 首次启动,文件不存在,正常
|
||||
} else {
|
||||
console.warn('[uds-auth:RolesStore] Failed to load roles file:', err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_markDirty() {
|
||||
this._dirty = true
|
||||
if (this._saveTimer) return
|
||||
this._saveTimer = setTimeout(() => this._save(), 2000)
|
||||
}
|
||||
|
||||
async _save() {
|
||||
this._saveTimer = null
|
||||
if (!this._dirty || !this._rolesFile) return
|
||||
this._dirty = false
|
||||
try {
|
||||
const data = {
|
||||
roles: Object.fromEntries(this._roles),
|
||||
fallbackPasswordHash: this._fallbackPasswordHash,
|
||||
savedAt: new Date().toISOString(),
|
||||
}
|
||||
await mkdir(dirname(this._rolesFile), { recursive: true })
|
||||
await writeFile(this._rolesFile, JSON.stringify(data, null, 2), 'utf-8')
|
||||
} catch (err) {
|
||||
console.warn('[uds-auth:RolesStore] Failed to save roles file:', err.message)
|
||||
}
|
||||
}
|
||||
|
||||
// === 首次部署 bootstrap ===
|
||||
|
||||
/**
|
||||
* 原子 check-and-set: 第一个登录的用户 = super_admin
|
||||
* 返回 { role, bootstrapped } bootstrapped=true 表示本次是 bootstrap
|
||||
*/
|
||||
async bootstrapFirstUser(empNo) {
|
||||
// 用锁保证原子性
|
||||
this._firstBootLock = this._firstBootLock.then(async () => {
|
||||
if (this._roles.size === 0) {
|
||||
this._roles.set(empNo, ROLES.SUPER_ADMIN)
|
||||
this._markDirty()
|
||||
return { role: ROLES.SUPER_ADMIN, bootstrapped: true }
|
||||
}
|
||||
if (!this._roles.has(empNo)) {
|
||||
this._roles.set(empNo, ROLES.USER)
|
||||
this._markDirty()
|
||||
return { role: ROLES.USER, bootstrapped: false }
|
||||
}
|
||||
return { role: this._roles.get(empNo), bootstrapped: false }
|
||||
})
|
||||
return this._firstBootLock
|
||||
}
|
||||
|
||||
// === 角色查询 ===
|
||||
|
||||
getRole(empNo) {
|
||||
if (empNo === 'administrator') return ROLES.FALLBACK_ADMIN
|
||||
return this._roles.get(empNo) || ROLES.USER
|
||||
}
|
||||
|
||||
hasRole(empNo) {
|
||||
return this._roles.has(empNo)
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return Array.from(this._roles.entries()).map(([empNo, role]) => ({ empNo, role }))
|
||||
}
|
||||
|
||||
async countByRole(role) {
|
||||
let n = 0
|
||||
for (const r of this._roles.values()) if (r === role) n++
|
||||
return n
|
||||
}
|
||||
|
||||
// === 角色管理 (super_admin only) ===
|
||||
|
||||
/**
|
||||
* 设置用户角色
|
||||
* 保护性 invariant: 至少保留 1 个 super_admin
|
||||
*/
|
||||
async setRole(empNo, newRole, currentAdminRole) {
|
||||
if (currentAdminRole !== ROLES.SUPER_ADMIN) {
|
||||
throw new Error('只有超级管理员可以修改角色')
|
||||
}
|
||||
|
||||
// invariant: 不能让系统变成 0 个 super_admin
|
||||
const currentRole = this._roles.get(empNo)
|
||||
if (currentRole === ROLES.SUPER_ADMIN && newRole !== ROLES.SUPER_ADMIN) {
|
||||
const superAdmins = await this.countByRole(ROLES.SUPER_ADMIN)
|
||||
if (superAdmins <= 1) {
|
||||
throw new Error('系统至少需要 1 个超级管理员,不能降级最后一个')
|
||||
}
|
||||
}
|
||||
|
||||
this._roles.set(empNo, newRole)
|
||||
this._markDirty()
|
||||
return true
|
||||
}
|
||||
|
||||
/** 删除用户 */
|
||||
async removeUser(empNo, currentAdminRole) {
|
||||
if (currentAdminRole !== ROLES.SUPER_ADMIN) {
|
||||
throw new Error('只有超级管理员可以删除用户')
|
||||
}
|
||||
const currentRole = this._roles.get(empNo)
|
||||
if (currentRole === ROLES.SUPER_ADMIN) {
|
||||
const superAdmins = await this.countByRole(ROLES.SUPER_ADMIN)
|
||||
if (superAdmins <= 1) {
|
||||
throw new Error('系统至少需要 1 个超级管理员,不能删除最后一个')
|
||||
}
|
||||
}
|
||||
this._roles.delete(empNo)
|
||||
this._markDirty()
|
||||
return true
|
||||
}
|
||||
|
||||
/** 确保用户存在 (如果不存在设为 user) */
|
||||
ensureUser(empNo, currentAdminRole) {
|
||||
if (currentAdminRole !== ROLES.SUPER_ADMIN) {
|
||||
throw new Error('只有超级管理员可以添加用户')
|
||||
}
|
||||
if (!this._roles.has(empNo)) {
|
||||
this._roles.set(empNo, ROLES.USER)
|
||||
this._markDirty()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// === Fallback Administrator ===
|
||||
|
||||
setFallbackPassword(password, currentAdminRole) {
|
||||
if (currentAdminRole !== ROLES.SUPER_ADMIN) {
|
||||
throw new Error('只有超级管理员可以设置兜底管理员密码')
|
||||
}
|
||||
if (!password || password.length < 6) {
|
||||
throw new Error('密码至少 6 位')
|
||||
}
|
||||
this._fallbackPasswordHash = hashPassword(password)
|
||||
this._markDirty()
|
||||
return true
|
||||
}
|
||||
|
||||
clearFallbackPassword(currentAdminRole) {
|
||||
if (currentAdminRole !== ROLES.SUPER_ADMIN) {
|
||||
throw new Error('只有超级管理员可以清除兜底管理员密码')
|
||||
}
|
||||
this._fallbackPasswordHash = null
|
||||
this._markDirty()
|
||||
return true
|
||||
}
|
||||
|
||||
isFallbackEnabled() {
|
||||
return this._fallbackPasswordHash !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 fallback 密码 + rate limit
|
||||
* @returns {boolean}
|
||||
*/
|
||||
verifyFallback(password, ip) {
|
||||
if (!this._fallbackPasswordHash) return false
|
||||
const now = Date.now()
|
||||
|
||||
// rate limit: 5 tries per minute per IP
|
||||
let bucket = this._fallbackRateLimit.get(ip)
|
||||
if (!bucket || now > bucket.resetAt) {
|
||||
bucket = { count: 0, resetAt: now + 60_000 }
|
||||
this._fallbackRateLimit.set(ip, bucket)
|
||||
}
|
||||
bucket.count++
|
||||
if (bucket.count > 5) {
|
||||
return false // rate limited
|
||||
}
|
||||
|
||||
return hashPassword(password) === this._fallbackPasswordHash
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue