实现企微首次免密自动绑定(07-28-all-account-login-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-07-28 11:18:50 +08:00
父节点 0551212b78
当前提交 9b12451b85
共修改 22 个文件,包含 1764 行新增和 132 行删除
@@ -0,0 +1,473 @@
#!/usr/bin/env node
const crypto = require("node:crypto");
const { createPrismaClient } = require("../prismaClient");
const { hashPassword } = require("../utils/crypto");
const { validatePassword } = require("../utils/passwordPolicy");
const PROTECTED_ADMIN_ROLES = ["admin", "system_admin"];
const APPLY_CONFIRMATION = "ACTIVE_ADMIN_BREAK_GLASS";
function writeResult(result) {
process.stdout.write(`${JSON.stringify(result)}\n`);
}
async function readPasswordFromStdin() {
if (process.stdin.isTTY) {
throw new Error("password_stdin_required");
}
process.stdin.setEncoding("utf8");
let input = "";
for await (const chunk of process.stdin) {
input += chunk;
if (input.length > 1024) {
throw new Error("password_input_too_long");
}
}
const password = input.replace(/[\r\n]+$/, "");
if (!password || password !== password.trim()) {
throw new Error("password_input_invalid");
}
const errors = validatePassword(password);
if (errors.length > 0) {
throw new Error("password_policy_failed");
}
return password;
}
async function findActiveAdmins(prisma) {
return prisma.employee.findMany({
where: {
status: "active",
archived_at: null,
roles: { some: { role_code: { in: PROTECTED_ADMIN_ROLES } } },
},
select: { id: true, token_version: true },
take: 2,
});
}
async function preflight(prisma) {
const admins = await findActiveAdmins(prisma);
return {
status: admins.length === 1 ? "PASS" : "BLOCKED",
activeAdminMatches: admins.length,
exactTarget: admins.length === 1,
};
}
async function verifyState(prisma) {
const admins = await findActiveAdmins(prisma);
if (admins.length !== 1) {
return {
status: "BLOCKED",
activeAdminMatches: admins.length,
exactTarget: false,
};
}
const target = admins[0];
const [
employee,
activeSessions,
activePasswordResetCodes,
activeEmailLoginCodes,
auditEntries,
] = await Promise.all([
prisma.employee.findUnique({
where: { id: target.id },
select: {
must_change_password: true,
login_failed_count: true,
login_locked_until: true,
email_login_code_failed_count: true,
email_login_code_locked_until: true,
password_reset_failed_count: true,
password_reset_locked_until: true,
token_version: true,
},
}),
prisma.authSession.count({
where: { employee_id: target.id, revoked_at: null },
}),
prisma.passwordResetCode.count({
where: { employee_id: target.id, invalidated_at: null },
}),
prisma.emailLoginCode.count({
where: { employee_id: target.id, used_at: null },
}),
prisma.auditLog.count({
where: {
actor_id: target.id,
target_id: target.id,
action: "SYSTEM_ADMIN_BREAK_GLASS_PASSWORD_RESET",
},
}),
]);
const locksCleared = Boolean(
employee &&
employee.login_failed_count === 0 &&
employee.login_locked_until === null &&
employee.email_login_code_failed_count === 0 &&
employee.email_login_code_locked_until === null &&
employee.password_reset_failed_count === 0 &&
employee.password_reset_locked_until === null,
);
const pass = Boolean(
employee?.must_change_password === true &&
locksCleared &&
activeSessions === 0 &&
activePasswordResetCodes === 0 &&
activeEmailLoginCodes === 0 &&
auditEntries > 0,
);
return {
status: pass ? "PASS" : "BLOCKED",
activeAdminMatches: 1,
exactTarget: true,
mustChangePassword: employee?.must_change_password === true,
locksCleared,
activeSessions,
activePasswordResetCodes,
activeEmailLoginCodes,
auditPresent: auditEntries > 0,
};
}
async function verifyCompletedPasswordChange(prisma) {
const admins = await findActiveAdmins(prisma);
if (admins.length !== 1) {
return {
status: "BLOCKED",
activeAdminMatches: admins.length,
exactTarget: false,
};
}
const target = admins[0];
const [employee, activeSessions, passwordChangeAudits, breakGlassAudits] =
await Promise.all([
prisma.employee.findUnique({
where: { id: target.id },
select: {
must_change_password: true,
login_failed_count: true,
login_locked_until: true,
email_login_code_failed_count: true,
email_login_code_locked_until: true,
password_reset_failed_count: true,
password_reset_locked_until: true,
},
}),
prisma.authSession.count({
where: { employee_id: target.id, revoked_at: null },
}),
prisma.auditLog.count({
where: {
actor_id: target.id,
target_id: target.id,
action: "AUTH_PASSWORD_CHANGED",
},
}),
prisma.auditLog.count({
where: {
actor_id: target.id,
target_id: target.id,
action: "SYSTEM_ADMIN_BREAK_GLASS_PASSWORD_RESET",
},
}),
]);
const locksCleared = Boolean(
employee &&
employee.login_failed_count === 0 &&
employee.login_locked_until === null &&
employee.email_login_code_failed_count === 0 &&
employee.email_login_code_locked_until === null &&
employee.password_reset_failed_count === 0 &&
employee.password_reset_locked_until === null,
);
const pass = Boolean(
employee?.must_change_password === false &&
locksCleared &&
activeSessions === 1 &&
passwordChangeAudits > 0 &&
breakGlassAudits > 0,
);
return {
status: pass ? "PASS" : "BLOCKED",
activeAdminMatches: 1,
exactTarget: true,
mustChangePassword: employee?.must_change_password === true,
locksCleared,
activeSessions,
passwordChangeAuditPresent: passwordChangeAudits > 0,
breakGlassAuditPresent: breakGlassAudits > 0,
};
}
async function diagnoseWecomBinding(prisma, now = new Date()) {
const admins = await findActiveAdmins(prisma);
if (admins.length !== 1) {
return {
status: "BLOCKED",
activeAdminMatches: admins.length,
exactTarget: false,
};
}
const target = admins[0];
const since = new Date(now.getTime() - 30 * 60 * 1000);
const [binding, recentAudits, latestDirectorySync, linkedHrSyncItems] =
await Promise.all([
prisma.wecomIdentityBinding.findUnique({
where: { employee_id: target.id },
select: { employee_id: true },
}),
prisma.auditLog.findMany({
where: {
actor_id: target.id,
created_at: { gte: since },
action: {
in: [
"AUTH_LOGIN_SUCCESS",
"AUTH_LOGIN_FAILED",
"AUTH_LOGIN_LOCKED",
"AUTH_WECOM_BIND",
"AUTH_WECOM_LOGIN_SUCCESS",
],
},
},
select: { action: true, user_agent: true },
}),
prisma.wecomDirectorySyncRun.findFirst({
where: { employee_id: target.id, external_user_id: { not: null } },
orderBy: { created_at: "desc" },
select: { external_user_id: true, status: true },
}),
prisma.wecomHrMasterSyncItem.count({
where: { employee_id: target.id, status: "success" },
}),
]);
const directoryUserIdConflicts = latestDirectorySync?.external_user_id
? await prisma.wecomIdentityBinding.count({
where: {
external_user_id: latestDirectorySync.external_user_id,
NOT: { employee_id: target.id },
},
})
: 0;
const passwordLogins = recentAudits.filter(
(audit) => audit.action === "AUTH_LOGIN_SUCCESS",
);
const wecomWebViewPasswordLogins = passwordLogins.filter((audit) =>
/wxwork|micromessenger/i.test(audit.user_agent ?? ""),
);
const failedPasswordLogins = recentAudits.filter((audit) =>
["AUTH_LOGIN_FAILED", "AUTH_LOGIN_LOCKED"].includes(audit.action),
);
const failedWecomWebViewPasswordLogins = failedPasswordLogins.filter(
(audit) => /wxwork|micromessenger/i.test(audit.user_agent ?? ""),
);
return {
status: "PASS",
activeAdminMatches: 1,
exactTarget: true,
targetBound: Boolean(binding),
directoryUserIdPresent: Boolean(latestDirectorySync?.external_user_id),
latestDirectorySyncSuccessful: latestDirectorySync?.status === "success",
directoryUserIdAlreadyBoundElsewhere: directoryUserIdConflicts > 0,
linkedSuccessfulHrSyncPresent: linkedHrSyncItems > 0,
recentPasswordLogins: passwordLogins.length,
recentPasswordLoginsFromWecomWebView: wecomWebViewPasswordLogins.length,
recentFailedPasswordLogins: failedPasswordLogins.length,
recentFailedPasswordLoginsFromWecomWebView:
failedWecomWebViewPasswordLogins.length,
recentWecomBindAudits: recentAudits.filter(
(audit) => audit.action === "AUTH_WECOM_BIND",
).length,
recentWecomLoginAudits: recentAudits.filter(
(audit) => audit.action === "AUTH_WECOM_LOGIN_SUCCESS",
).length,
};
}
async function applyReset(prisma, password) {
const admins = await findActiveAdmins(prisma);
if (admins.length !== 1) {
throw new Error("active_admin_target_not_unique");
}
const target = admins[0];
const passwordHash = await hashPassword(password);
const now = new Date();
const requestId = `ops-break-glass-${crypto.randomUUID()}`;
const result = await prisma.$transaction(async (tx) => {
const current = await tx.employee.findUnique({
where: { id: target.id },
select: {
status: true,
archived_at: true,
token_version: true,
roles: { select: { role_code: true } },
},
});
const protectedRoleCount =
current?.roles.filter((role) =>
PROTECTED_ADMIN_ROLES.includes(role.role_code),
).length ?? 0;
if (
!current ||
current.status !== "active" ||
current.archived_at !== null ||
protectedRoleCount === 0 ||
current.token_version !== target.token_version
) {
throw new Error("active_admin_target_changed");
}
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 },
updated_by: target.id,
},
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: "SYSTEM_ADMIN_BREAK_GLASS_PASSWORD_RESET",
target_type: "employee",
target_id: target.id,
ip_address: null,
user_agent: "controlled-ops-script",
extra: { credentialRecorded: false },
},
});
return {
status: "PASS",
activeAdminMatches: 1,
passwordUpdated: true,
mustChangePassword: employee.must_change_password === true,
tokenVersionIncremented:
employee.token_version === target.token_version + 1,
locksCleared: true,
revokedSessions: revokedSessions.count,
invalidatedPasswordResetCodes: invalidatedPasswordResetCodes.count,
consumedEmailLoginCodes: consumedEmailLoginCodes.count,
auditCreated: true,
};
});
return result;
}
async function main() {
const { prisma, pool } = createPrismaClient();
try {
if (process.argv.includes("--preflight")) {
writeResult(await preflight(prisma));
return;
}
if (process.argv.includes("--verify")) {
writeResult(await verifyState(prisma));
return;
}
if (process.argv.includes("--verify-password-changed")) {
writeResult(await verifyCompletedPasswordChange(prisma));
return;
}
if (process.argv.includes("--diagnose-wecom-binding")) {
writeResult(await diagnoseWecomBinding(prisma));
return;
}
const applyIndex = process.argv.indexOf("--apply");
if (applyIndex < 0 || process.argv[applyIndex + 1] !== APPLY_CONFIRMATION) {
throw new Error("explicit_confirmation_required");
}
const password = await readPasswordFromStdin();
writeResult(await applyReset(prisma, password));
} finally {
await prisma.$disconnect();
await pool.end();
}
}
if (require.main === module) {
main().catch((error) => {
const safeErrorCategory =
typeof error?.message === "string" &&
[
"password_stdin_required",
"password_input_too_long",
"password_input_invalid",
"password_policy_failed",
"active_admin_target_not_unique",
"active_admin_target_changed",
"explicit_confirmation_required",
].includes(error.message)
? error.message
: typeof error?.code === "string"
? error.code
: typeof error?.name === "string"
? error.name
: "runtime_error";
writeResult({
status: "BLOCKED",
error: "break_glass_operation_failed",
category: safeErrorCategory,
});
process.exitCode = 1;
});
}
module.exports = {
applyReset,
diagnoseWecomBinding,
findActiveAdmins,
preflight,
readPasswordFromStdin,
verifyCompletedPasswordChange,
verifyState,
};