Files
2026-10-27 17:44:41 +08:00

74 lines
4.9 KiB
JavaScript

const IAM = '/api/platform/v1/iam'
const id = value => String(value ?? '')
const same = (a, b) => JSON.stringify([...a].sort()) === JSON.stringify([...b].sort())
export const POOL_ACTOR_SLOTS = ['pool.sales.0', 'pool.sales.1', 'pool.dispatch.0', 'pool.dispatch.1', 'pool.fulfillment.0']
export const UI_ACTOR_SLOTS = ['pool-ui.SALES_A', 'pool-ui.SALES_B', 'pool-ui.MANAGER']
export const COST_ACTOR_SLOTS = ['fleet-cost.reader', 'fleet-cost.denied']
export function requireFixedActors(options, slots) {
const actors = options?.fixedActors
const users = new Set(), memberships = new Set()
for (const slot of slots) {
const actor = actors?.[slot]
if (!actor || !['membershipId', 'roleId'].every(key => typeof actor[key] === 'string' && /^[1-9]\d*$/.test(actor[key]))
|| !['username', 'password', 'roleCode'].every(key => typeof actor[key] === 'string' && actor[key].trim()))
throw Error(`固定测试账号 ${slot} 配置不完整;禁止自动创建角色或账号`)
if (users.has(actor.username) || memberships.has(actor.membershipId))
throw Error('隔离场景必须使用不同的固定账号,不能以同一账号代替')
users.add(actor.username); memberships.add(actor.membershipId)
}
return actors
}
/** Only validates existing platform identities and opens/closes its own sessions. Never changes roles or accounts. */
export async function openFixedActor(context, slot, expectedCodes) {
const actor = requireFixedActors(context.options, [slot])[slot]
async function request(method, path, body, token = context.token) {
const response = await context.request(method, path, body, token)
if (response?.http !== 200 || String(response.body?.code) !== '200') throw Error(`固定账号 ${slot} 核验失败:${method} ${path.split('?')[0]}`)
return response.body.data
}
const roles = await request('GET', `${IAM}/roles`)
const matches = Array.isArray(roles) ? roles.filter(row => id(row.id) === actor.roleId) : []
if (matches.length !== 1 || matches[0].roleCode !== actor.roleCode || matches[0].status !== 'ACTIVE')
throw Error(`固定账号 ${slot} 的角色身份或启用状态不匹配,不自动改角色`)
let user, total
for (let page = 1; page <= 100; page++) {
// IAM keyword filters profile fields/member numbers, not the login name. Match the configured identity in the authoritative directory.
const data = await request('GET', `${IAM}/users?${new URLSearchParams({ page: String(page), size: '100' })}`)
if (!Array.isArray(data?.records) || Number(data.page) !== page || Number(data.size) !== 100
|| !Number.isSafeInteger(Number(data.total)) || Number(data.total) < 0 || data.records.length > 100
|| (total !== undefined && total !== Number(data.total))) throw Error('固定账号查询分页不完整或发生变化')
total ??= Number(data.total)
const candidates = data.records.filter(row => id(row.membershipId) === actor.membershipId && row.username === actor.username)
if (candidates.length > 1) throw Error('固定账号查询身份重复')
if (candidates.length) { user = candidates[0]; break }
if (page * 100 >= total) break
if (!data.records.length) throw Error('固定账号查询分页缺页')
}
if (!user || user.status !== 'ACTIVE' || id(user.tenantId) !== '0'
|| !Array.isArray(user.roleIds) || !same(user.roleIds.map(id), [actor.roleId]))
throw Error(`固定账号 ${slot} 的成员、平台归属、状态或角色绑定不匹配,不自动修改账号`)
const session = await request('POST', '/api/auth/login', { loginId: actor.username, password: actor.password }, '')
let closed = false
async function close() {
if (closed) return
if (!session?.accessToken || !session.refreshToken) throw Error('固定账号会话不完整,无法确认退出')
const response = await context.request('POST', '/api/auth/logout', { refreshToken: session.refreshToken }, session.accessToken)
if (!(response?.http === 200 && String(response.body?.code) === '200')
&& !([200, 401].includes(response?.http) && String(response.body?.code) === 'A0401')) throw Error('固定账号退出未确认,保留现场')
closed = true
}
try {
if (!session?.accessToken || !session.refreshToken || session.accessToken === context.token) throw Error('固定账号缺少独立完整会话')
const codes = await request('GET', `${IAM}/authorization/permissions`, undefined, session.accessToken)
if (!Array.isArray(codes) || !same(codes, expectedCodes)) throw Error(`固定账号 ${slot} 实际权限不匹配,不自动授权或扩大权限`)
} catch (error) {
try { await close() } catch (cleanupError) { throw new AggregateError([error, cleanupError], '固定账号核验和退出均失败') }
throw error
}
return { membershipId: actor.membershipId, username: actor.username, roleId: actor.roleId, roleCode: actor.roleCode,
token: session.accessToken, refreshToken: session.refreshToken, reused: true, close }
}