实现企微首次免密自动绑定(07-28-all-account-login-readiness)
这个提交包含在:
@@ -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,
|
||||
};
|
||||
在新工单中引用
屏蔽一个用户