实现(08-10-production-employee-smoke-break-glass-reset): 增加员工Smoke受控重置工具
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-10 21:01:59 +08:00
父节点 b807762c52
当前提交 ec68256588
共修改 10 个文件,包含 1310 行新增和 0 行删除
@@ -646,6 +646,7 @@ if (require.main === module) {
module.exports = {
DEFAULT_TARGETS,
applyTarget,
execute,
isSafeOaOnlySmokeTarget,
isSmokeLikeValue,
@@ -0,0 +1,503 @@
#!/usr/bin/env node
const crypto = require("node:crypto");
const { createPrismaClient } = require("../prismaClient");
const { resolvePermissionsForRoles } = require("../constants/permissions");
const { hashPassword } = require("../utils/crypto");
const { readPasswordFromStdin } = require("./reset-active-admin-password");
const { isSmokeLikeValue } = require("./repair-oa-only-smoke-accounts");
const APPLY_CONFIRMATION = "EMPLOYEE_SMOKE_BREAK_GLASS";
const AUDIT_ACTION = "SYSTEM_EMPLOYEE_SMOKE_BREAK_GLASS_PASSWORD_RESET";
const PASSWORD_CHANGED_ACTION = "AUTH_PASSWORD_CHANGED";
const EXPECTED_ROLES = Object.freeze(["employee"]);
const FORBIDDEN_PERMISSIONS = Object.freeze([
"*",
"project.manage",
"project.view_all",
"system.settings",
]);
const PRIVACY =
"target count/status/role/permission booleans only; no password/token/secret/identifier/full email/full phone";
function writeResult(result) {
process.stdout.write(`${JSON.stringify({ ...result, privacy: PRIVACY })}\n`);
}
function sortedRoleCodes(row) {
const roles = Array.isArray(row?.roles) ? row.roles : [];
return roles
.map((item) => (typeof item === "string" ? item : item?.role_code))
.filter((item) => typeof item === "string")
.sort();
}
function rolesAreExactEmployee(row) {
const roles = sortedRoleCodes(row);
return roles.length === EXPECTED_ROLES.length && roles[0] === EXPECTED_ROLES[0];
}
function isSmokeRow(row) {
return [row?.employee_no, row?.email, row?.name].some(isSmokeLikeValue);
}
function isIdentifierReady(row) {
return [row?.email, row?.phone].some(
(value) => typeof value === "string" && value.trim().length > 0,
);
}
function buildPermissionState(row) {
const permissionCodes = resolvePermissionsForRoles(sortedRoleCodes(row));
return {
hasWildcard: permissionCodes.includes("*"),
hasProjectManage: permissionCodes.includes("project.manage"),
hasProjectViewAll: permissionCodes.includes("project.view_all"),
hasSystemSettings: permissionCodes.includes("system.settings"),
};
}
function allManagementPermissionsAbsent(permissionState) {
return FORBIDDEN_PERMISSIONS.every((permission) => {
if (permission === "*") return !permissionState.hasWildcard;
if (permission === "project.manage") return !permissionState.hasProjectManage;
if (permission === "project.view_all") return !permissionState.hasProjectViewAll;
return !permissionState.hasSystemSettings;
});
}
function buildTargetState(row, { expectedMustChangePassword }) {
const permissionState = buildPermissionState(row);
const locksCleared = Boolean(
row &&
row.login_failed_count === 0 &&
row.login_failed_first_at === null &&
row.login_locked_until === null &&
row.email_login_code_failed_count === 0 &&
row.email_login_code_failed_first_at === null &&
row.email_login_code_locked_until === null &&
row.password_reset_failed_count === 0 &&
row.password_reset_failed_first_at === null &&
row.password_reset_locked_until === null,
);
const state = {
activeEmployment: Boolean(
row &&
row.status === "active" &&
row.employment_status === "active" &&
row.archived_at === null,
),
smokeTarget: Boolean(row && isSmokeRow(row)),
identifierReady: Boolean(row && isIdentifierReady(row)),
mustChangePassword: Boolean(row?.must_change_password),
mustChangePasswordMatches:
Boolean(row) && row.must_change_password === expectedMustChangePassword,
locksCleared,
rolesExact: Boolean(row && rolesAreExactEmployee(row)),
...permissionState,
};
return {
...state,
contractManagePermissionsAbsent: allManagementPermissionsAbsent(state),
};
}
function targetStatePasses(state) {
return Boolean(
state.activeEmployment &&
state.smokeTarget &&
state.identifierReady &&
state.mustChangePasswordMatches &&
state.locksCleared &&
state.rolesExact &&
state.contractManagePermissionsAbsent,
);
}
const TARGET_SELECT = Object.freeze({
id: true,
token_version: true,
employee_no: true,
name: true,
email: true,
phone: true,
status: true,
employment_status: true,
archived_at: true,
must_change_password: true,
login_failed_count: true,
login_failed_first_at: true,
login_locked_until: true,
email_login_code_failed_count: true,
email_login_code_failed_first_at: true,
email_login_code_locked_until: true,
password_reset_failed_count: true,
password_reset_failed_first_at: true,
password_reset_locked_until: true,
roles: { select: { role_code: true } },
});
async function findPreflightCandidates(prisma) {
const rows = await prisma.employee.findMany({
where: {
must_change_password: false,
roles: { some: { role_code: "employee" } },
},
select: TARGET_SELECT,
});
return rows.filter((row) => isSmokeRow(row) && rolesAreExactEmployee(row));
}
async function findAuditTargets(prisma) {
const auditTargets = await prisma.auditLog.findMany({
where: {
action: AUDIT_ACTION,
target_type: "employee",
target_id: { not: null },
},
select: { target_id: true },
distinct: ["target_id"],
take: 2,
});
const targetIds = auditTargets.map((item) => item.target_id).filter(Boolean);
if (targetIds.length === 0) return [];
return prisma.employee.findMany({
where: { id: { in: targetIds } },
select: TARGET_SELECT,
});
}
function summarizeTarget(rows, expectedMustChangePassword) {
const exactTarget = rows.length === 1;
const targetState = buildTargetState(exactTarget ? rows[0] : null, {
expectedMustChangePassword,
});
return {
targetMatches: rows.length,
exactTarget,
...targetState,
pass: exactTarget && targetStatePasses(targetState),
};
}
async function preflight(prisma) {
const summary = summarizeTarget(await findPreflightCandidates(prisma), false);
return {
status: summary.pass ? "PASS" : "BLOCKED",
...summary,
};
}
async function countTargetAuthenticationState(prisma, targetId) {
const [activeSessions, activePasswordResetCodes, activeEmailLoginCodes, auditEntries] =
await Promise.all([
prisma.authSession.count({
where: { employee_id: targetId, revoked_at: null },
}),
prisma.passwordResetCode.count({
where: { employee_id: targetId, invalidated_at: null },
}),
prisma.emailLoginCode.count({
where: { employee_id: targetId, used_at: null },
}),
prisma.auditLog.count({
where: {
actor_id: targetId,
target_id: targetId,
action: AUDIT_ACTION,
},
}),
]);
return {
activeSessions,
activePasswordResetCodes,
activeEmailLoginCodes,
auditPresent: auditEntries > 0,
};
}
async function verifyState(prisma) {
const rows = await findAuditTargets(prisma);
const summary = summarizeTarget(rows, true);
if (!summary.exactTarget) {
return { status: "BLOCKED", ...summary };
}
const authState = await countTargetAuthenticationState(prisma, rows[0].id);
const pass = Boolean(
summary.pass &&
authState.activeSessions === 0 &&
authState.activePasswordResetCodes === 0 &&
authState.activeEmailLoginCodes === 0 &&
authState.auditPresent,
);
return {
status: pass ? "PASS" : "BLOCKED",
...summary,
...authState,
pass,
};
}
async function verifyCompletedPasswordChange(prisma) {
const rows = await findAuditTargets(prisma);
const summary = summarizeTarget(rows, false);
if (!summary.exactTarget) {
return { status: "BLOCKED", ...summary };
}
const targetId = rows[0].id;
const [activeSessions, passwordChangeAudits, breakGlassAudits] = await Promise.all([
prisma.authSession.count({
where: { employee_id: targetId, revoked_at: null },
}),
prisma.auditLog.count({
where: {
actor_id: targetId,
target_id: targetId,
action: PASSWORD_CHANGED_ACTION,
},
}),
prisma.auditLog.count({
where: {
actor_id: targetId,
target_id: targetId,
action: AUDIT_ACTION,
},
}),
]);
const pass = Boolean(
summary.pass &&
activeSessions === 1 &&
passwordChangeAudits > 0 &&
breakGlassAudits > 0,
);
return {
status: pass ? "PASS" : "BLOCKED",
...summary,
activeSessions,
passwordChangeAuditPresent: passwordChangeAudits > 0,
breakGlassAuditPresent: breakGlassAudits > 0,
pass,
};
}
async function lockTargetSnapshot(tx, targetId) {
const employeeRows = await tx.$queryRaw`
SELECT
id,
token_version,
employee_no,
name,
email,
phone,
status,
employment_status,
archived_at,
must_change_password,
login_failed_count,
login_failed_first_at,
login_locked_until,
email_login_code_failed_count,
email_login_code_failed_first_at,
email_login_code_locked_until,
password_reset_failed_count,
password_reset_failed_first_at,
password_reset_locked_until
FROM "employees"
WHERE id = ${targetId}::uuid
FOR UPDATE
`;
if (!Array.isArray(employeeRows) || employeeRows.length !== 1) return null;
const roleRows = await tx.$queryRaw`
SELECT role_code::text
FROM "employee_roles"
WHERE employee_id = ${targetId}::uuid
ORDER BY role_code::text
FOR UPDATE
`;
return {
...employeeRows[0],
roles: Array.isArray(roleRows) ? roleRows : [],
};
}
function assertLockedTargetUnchanged(current, target) {
const state = buildTargetState(current, { expectedMustChangePassword: false });
if (
!current ||
current.id !== target.id ||
current.token_version !== target.token_version ||
!targetStatePasses(state)
) {
throw new Error("employee_smoke_target_changed");
}
}
async function applyReset(prisma, password) {
const candidates = await findPreflightCandidates(prisma);
const summary = summarizeTarget(candidates, false);
if (!summary.pass) {
throw new Error("employee_smoke_target_not_unique_or_safe");
}
const target = candidates[0];
const passwordHash = await hashPassword(password);
const now = new Date();
const requestId = `ops-employee-smoke-break-glass-${crypto.randomUUID()}`;
return prisma.$transaction(
async (tx) => {
const current = await lockTargetSnapshot(tx, target.id);
assertLockedTargetUnchanged(current, target);
const employee = await tx.employee.update({
where: { id: target.id },
data: {
password_hash: passwordHash,
must_change_password: true,
login_failed_count: 0,
login_failed_first_at: null,
login_locked_until: null,
email_login_code_failed_count: 0,
email_login_code_failed_first_at: null,
email_login_code_locked_until: null,
password_reset_failed_count: 0,
password_reset_failed_first_at: null,
password_reset_locked_until: null,
password_reset_request_count: 0,
password_reset_request_first_at: null,
password_reset_last_requested_at: null,
token_version: { increment: 1 },
},
select: { must_change_password: true, token_version: true },
});
const revokedSessions = await tx.authSession.updateMany({
where: { employee_id: target.id, revoked_at: null },
data: { revoked_at: now, last_seen_at: now },
});
const invalidatedPasswordResetCodes = await tx.passwordResetCode.updateMany({
where: { employee_id: target.id, invalidated_at: null },
data: { invalidated_at: now },
});
const consumedEmailLoginCodes = await tx.emailLoginCode.updateMany({
where: { employee_id: target.id, used_at: null },
data: { used_at: now },
});
await tx.auditLog.create({
data: {
request_id: requestId,
actor_id: target.id,
action: AUDIT_ACTION,
target_type: "employee",
target_id: target.id,
ip_address: null,
user_agent: "controlled-ops-script",
extra: { credentialRecorded: false },
},
});
return {
status: "PASS",
targetMatches: 1,
exactTarget: true,
passwordUpdated: true,
mustChangePassword: employee.must_change_password === true,
tokenVersionIncremented:
employee.token_version === target.token_version + 1,
locksCleared: true,
rolesExact: true,
contractManagePermissionsAbsent: true,
revokedSessions: revokedSessions.count,
invalidatedPasswordResetCodes: invalidatedPasswordResetCodes.count,
consumedEmailLoginCodes: consumedEmailLoginCodes.count,
auditCreated: true,
};
},
{ isolationLevel: "Serializable" },
);
}
function resolveMode(argv) {
if (argv.length === 0 || (argv.length === 1 && ["--dry-run", "--preflight"].includes(argv[0]))) {
return { mode: "preflight" };
}
if (argv.length === 1 && argv[0] === "--verify") {
return { mode: "verify" };
}
if (argv.length === 1 && argv[0] === "--verify-password-changed") {
return { mode: "verify_password_changed" };
}
if (argv[0] === "--apply") {
if (argv.length !== 2 || argv[1] !== APPLY_CONFIRMATION) {
throw new Error("explicit_confirmation_required");
}
return { mode: "apply" };
}
throw new Error("unsupported_arguments");
}
async function main() {
const operation = resolveMode(process.argv.slice(2));
const { prisma, pool } = createPrismaClient();
try {
if (operation.mode === "preflight") {
writeResult(await preflight(prisma));
return;
}
if (operation.mode === "verify") {
writeResult(await verifyState(prisma));
return;
}
if (operation.mode === "verify_password_changed") {
writeResult(await verifyCompletedPasswordChange(prisma));
return;
}
const password = await readPasswordFromStdin();
writeResult(await applyReset(prisma, password));
} finally {
await prisma.$disconnect();
await pool.end();
}
}
if (require.main === module) {
main().catch((error) => {
const allowedCategories = new Set([
"password_stdin_required",
"password_input_too_long",
"password_input_invalid",
"password_policy_failed",
"employee_smoke_target_not_unique_or_safe",
"employee_smoke_target_changed",
"explicit_confirmation_required",
"unsupported_arguments",
]);
const category = allowedCategories.has(error?.message)
? error.message
: typeof error?.code === "string"
? error.code
: typeof error?.name === "string"
? error.name
: "runtime_error";
writeResult({
status: "BLOCKED",
error: "employee_smoke_break_glass_operation_failed",
category,
});
process.exitCode = 1;
});
}
module.exports = {
APPLY_CONFIRMATION,
AUDIT_ACTION,
applyReset,
buildTargetState,
findAuditTargets,
findPreflightCandidates,
preflight,
resolveMode,
rolesAreExactEmployee,
verifyCompletedPasswordChange,
verifyState,
};