mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: move totp and passkeys into plugins (C1)
This commit is contained in:
1 parent
f4f26a9b8b
commit
5c18435105
235 files changed
+22545
-5610
No files matched your search
@@ -63,6 +63,7 @@ vi.mock("../../database/repositories/factory.js", () => ({
|
||||
vi.mock("../../utils/system-secret-crypto.js", () => ({
|
||||
encryptSystemSecret: async (value: string) => `sysenc:${value}`,
|
||||
decryptSystemSecret: async (value: string) => value.replace(/^sysenc:/, ""),
|
||||
isSystemEncrypted: (value: string) => value.startsWith("sysenc:"),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/audit-logger.js", () => ({
|
||||
@@ -156,4 +157,20 @@ describe("ctx.secrets store", () => {
|
||||
});
|
||||
expect(state.rows.size).toBe(0);
|
||||
});
|
||||
|
||||
it("seals without an acting user and refuses without secrets:own", async () => {
|
||||
const denied = contextFor([]);
|
||||
await expect(denied.secrets.seal("v")).rejects.toThrow(/secrets:own/);
|
||||
await expect(denied.secrets.unseal("sysenc:v")).rejects.toThrow(
|
||||
/secrets:own/,
|
||||
);
|
||||
|
||||
const ctx = contextFor(["secrets:own"]);
|
||||
const sealed = await ctx.secrets.seal("totp-secret");
|
||||
expect(sealed).toBe("sysenc:totp-secret");
|
||||
expect(await ctx.secrets.unseal(sealed)).toBe("totp-secret");
|
||||
// A plain string was never sealed and must not read back as one.
|
||||
expect(await ctx.secrets.unseal("totp-secret")).toBeNull();
|
||||
expect(JSON.stringify(auditEntries)).not.toContain("totp-secret");
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,10 @@ const h = vi.hoisted(() => ({
|
||||
cleared: [] as string[],
|
||||
clearedAll: 0,
|
||||
pooled: [] as string[],
|
||||
sessionId: undefined as string | undefined,
|
||||
revoked: [] as Array<{ userId: string; except?: string }>,
|
||||
trustedCleared: [] as string[],
|
||||
policyError: null as Error | null,
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/logger.js", () => {
|
||||
@@ -29,7 +33,15 @@ vi.mock("../../plugins/permissions.js", async () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("../../plugins/actor.js", () => ({ getActor: () => h.actor }));
|
||||
vi.mock("../../plugins/actor.js", () => ({
|
||||
getActor: () => h.actor,
|
||||
getActorSessionId: () => h.sessionId,
|
||||
}));
|
||||
vi.mock("../../auth/core-auth.js", () => ({
|
||||
assertSecondFactorEnrollmentAllowed: () => {
|
||||
if (h.policyError) throw h.policyError;
|
||||
},
|
||||
}));
|
||||
vi.mock("../../hosts/ssh-connection-pool.js", () => ({
|
||||
withConnection: async (
|
||||
key: string,
|
||||
@@ -74,6 +86,17 @@ vi.mock("../../database/repositories/factory.js", () => ({
|
||||
},
|
||||
removeSecondFactor: async () => true,
|
||||
}),
|
||||
createCurrentSessionRepository: () => ({
|
||||
revokeAllForUser: async (userId: string, except?: string) => {
|
||||
h.revoked.push({ userId, except });
|
||||
return 0;
|
||||
},
|
||||
}),
|
||||
createCurrentTrustedDeviceRepository: () => ({
|
||||
deleteByUserId: async (userId: string) => {
|
||||
h.trustedCleared.push(userId);
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const { createPluginAuth, createPluginSsh } =
|
||||
@@ -359,4 +382,41 @@ describe("ctx.auth", () => {
|
||||
expect(getLoginMethod("corp-sso")).toBeUndefined();
|
||||
expect(getSecondFactor("fixture", "pin")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("revokes other sessions and trusted devices on enrolment, keeping the caller's session", async () => {
|
||||
h.granted = new Set(["auth:provide"]);
|
||||
h.factors = [];
|
||||
h.revoked = [];
|
||||
h.trustedCleared = [];
|
||||
h.policyError = null;
|
||||
h.actor = "user-1";
|
||||
h.sessionId = "session-a";
|
||||
const auth = createPluginAuth({
|
||||
manifest: manifest(["auth:provide"], { secondFactors: ["pin"] }),
|
||||
bag: new DisposableBag("fixture"),
|
||||
audit: vi.fn(async () => {}),
|
||||
});
|
||||
await auth.recordEnrollment("user-1", "pin");
|
||||
expect(h.revoked).toEqual([{ userId: "user-1", except: "session-a" }]);
|
||||
expect(h.trustedCleared).toEqual(["user-1"]);
|
||||
h.actor = undefined;
|
||||
h.sessionId = undefined;
|
||||
});
|
||||
|
||||
it("refuses enrolment when core policy forbids second factors", async () => {
|
||||
h.granted = new Set(["auth:provide"]);
|
||||
h.factors = [];
|
||||
const { LoginMethodError } = await import("@termix/plugin-sdk/backend");
|
||||
h.policyError = new LoginMethodError("password login is off", 409);
|
||||
const auth = createPluginAuth({
|
||||
manifest: manifest(["auth:provide"], { secondFactors: ["pin"] }),
|
||||
bag: new DisposableBag("fixture"),
|
||||
audit: vi.fn(async () => {}),
|
||||
});
|
||||
await expect(auth.recordEnrollment("user-1", "pin")).rejects.toMatchObject({
|
||||
status: 409,
|
||||
});
|
||||
expect(h.factors).toEqual([]);
|
||||
h.policyError = null;
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user