mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 07:35:24 +00:00
feat: drop the 2.8 columns, moves and aliases kept for a downgrade
This commit is contained in:
1 parent
d3d8269cb7
commit
61f46d2517
196 files changed
+29909
-10511
No files matched your search
@@ -96,9 +96,8 @@ it.each([false, true])(
|
||||
overwrite ? 19 : 71,
|
||||
expect.objectContaining({ pluginSettings: host.pluginSettings }),
|
||||
);
|
||||
expect(JSON.parse(saved.terminalConfig)).toEqual({
|
||||
macOptionIsMeta: false,
|
||||
});
|
||||
// The 2.8 terminalConfig reaches the plugin, never a host column.
|
||||
expect(saved).not.toHaveProperty("terminalConfig");
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -284,7 +284,7 @@ describe("stripSensitiveFields", () => {
|
||||
expect(result.hasKey).toBe(false);
|
||||
});
|
||||
|
||||
it("detects sudo password stored only in nested terminalConfig", () => {
|
||||
it("strips a sudo password nested in terminalConfig", () => {
|
||||
const result = stripSensitiveFields({
|
||||
name: "web",
|
||||
terminalConfig: {
|
||||
@@ -292,7 +292,7 @@ describe("stripSensitiveFields", () => {
|
||||
sudoPassword: "nested-only-sudo",
|
||||
},
|
||||
});
|
||||
expect(result.hasSudoPassword).toBe(true);
|
||||
expect(result.hasSudoPassword).toBe(false);
|
||||
expect(
|
||||
(result.terminalConfig as Record<string, unknown>).sudoPassword,
|
||||
).toBeUndefined();
|
||||
@@ -483,25 +483,6 @@ describe("transformHostResponse terminal fields", () => {
|
||||
expect(host.terminalConfig).toEqual({ keepaliveInterval: 20 });
|
||||
});
|
||||
|
||||
it("reads the options out of terminal_config before the boot copy", () => {
|
||||
const host = row({
|
||||
sshOptions: null,
|
||||
terminalConfig: JSON.stringify({ agentForwarding: true }),
|
||||
});
|
||||
expect(host.sshOptions).toEqual({ agentForwarding: true });
|
||||
});
|
||||
|
||||
it("surfaces a 2.8 sudo password for the sanitizers to strip", () => {
|
||||
const host = row({
|
||||
sudoPassword: null,
|
||||
terminalConfig: JSON.stringify({ sudoPassword: "legacy" }),
|
||||
});
|
||||
const stripped = stripSensitiveFields(host);
|
||||
expect(stripped.hasSudoPassword).toBe(true);
|
||||
expect(stripped).not.toHaveProperty("sudoPassword");
|
||||
expect(JSON.stringify(stripped)).not.toContain("legacy");
|
||||
});
|
||||
|
||||
it("hides the owner's agent socket from a shared recipient", () => {
|
||||
const shared = sanitizeHostForRecipient(
|
||||
row({
|
||||
|
||||
@@ -10,17 +10,11 @@ vi.mock("../../../database/repositories/factory.js", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const { mergeStoredTerminalFields, parseTerminalConfig } =
|
||||
const { mergeStoredSshOptions, parseTerminalConfig } =
|
||||
await import("../../../database/routes/host-terminal-fields.js");
|
||||
|
||||
beforeEach(() => {
|
||||
state.stored = {
|
||||
terminalConfig: JSON.stringify({
|
||||
theme: "nord",
|
||||
sudoPassword: "legacy-sudo",
|
||||
agentSocketPath: "/owner/agent",
|
||||
startupSnippetId: 1,
|
||||
}),
|
||||
sshOptions: JSON.stringify({ agentSocketPath: "/owner/agent" }),
|
||||
};
|
||||
});
|
||||
@@ -31,10 +25,10 @@ async function merge(
|
||||
sshOptions?: string,
|
||||
) {
|
||||
const row: Record<string, unknown> = {
|
||||
terminalConfig: "replaced wholesale",
|
||||
terminalConfig: "from the payload",
|
||||
...(sshOptions !== undefined ? { sshOptions } : {}),
|
||||
};
|
||||
const error = await mergeStoredTerminalFields(row, hostData, 1, "o", isOwner);
|
||||
const error = await mergeStoredSshOptions(row, hostData, 1, "o", isOwner);
|
||||
return { error, row };
|
||||
}
|
||||
|
||||
@@ -47,43 +41,32 @@ describe("parseTerminalConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("mergeStoredTerminalFields", () => {
|
||||
it("merges an editor save into the stored 2.8 keys instead of dropping them", async () => {
|
||||
describe("mergeStoredSshOptions", () => {
|
||||
it("never writes terminalConfig, which has no column", async () => {
|
||||
const { error, row } = await merge({
|
||||
terminalConfig: { startupSnippetId: 4 },
|
||||
});
|
||||
expect(error).toBeNull();
|
||||
expect(JSON.parse(row.terminalConfig as string)).toEqual({
|
||||
theme: "nord",
|
||||
sudoPassword: "legacy-sudo",
|
||||
agentSocketPath: "/owner/agent",
|
||||
startupSnippetId: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves terminal_config alone when the payload has none", async () => {
|
||||
const { row } = await merge({});
|
||||
expect(row).not.toHaveProperty("terminalConfig");
|
||||
});
|
||||
|
||||
it("drops the legacy sudo copy once the owner saves one on its own", async () => {
|
||||
const { row } = await merge({ sudoPassword: "" });
|
||||
expect(JSON.parse(row.terminalConfig as string)).not.toHaveProperty(
|
||||
"sudoPassword",
|
||||
it("leaves an owner's SSH options as sent", async () => {
|
||||
const { row } = await merge(
|
||||
{},
|
||||
true,
|
||||
JSON.stringify({ agentSocketPath: "/x" }),
|
||||
);
|
||||
expect(JSON.parse(row.sshOptions as string)).toEqual({
|
||||
agentSocketPath: "/x",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the owner's private keys through a shared editor's save", async () => {
|
||||
it("keeps the owner's private SSH options through a shared editor's save", async () => {
|
||||
const { row } = await merge(
|
||||
{ terminalConfig: { startupSnippetId: 2 } },
|
||||
{},
|
||||
false,
|
||||
JSON.stringify({ keepaliveInterval: 9 }),
|
||||
JSON.stringify({ keepaliveInterval: 9, agentSocketPath: "/editor" }),
|
||||
);
|
||||
expect(JSON.parse(row.terminalConfig as string)).toMatchObject({
|
||||
sudoPassword: "legacy-sudo",
|
||||
agentSocketPath: "/owner/agent",
|
||||
startupSnippetId: 2,
|
||||
});
|
||||
expect(JSON.parse(row.sshOptions as string)).toEqual({
|
||||
keepaliveInterval: 9,
|
||||
agentSocketPath: "/owner/agent",
|
||||
|
||||
@@ -13,10 +13,7 @@ interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
isOidc: boolean;
|
||||
passwordHash: string;
|
||||
oidcIdentifier?: string | null;
|
||||
clientId?: string;
|
||||
}
|
||||
|
||||
const h = vi.hoisted(() => ({
|
||||
@@ -30,14 +27,21 @@ const h = vi.hoisted(() => ({
|
||||
revoked: [] as string[],
|
||||
}));
|
||||
|
||||
function withIdentities(user: User | undefined) {
|
||||
if (!user) return null;
|
||||
return {
|
||||
...user,
|
||||
isExternal: h.identities.some((row) => row.userId === user.id),
|
||||
};
|
||||
}
|
||||
|
||||
vi.mock("../../../database/repositories/factory.js", () => ({
|
||||
createCurrentUserRepository: () => ({
|
||||
findById: async (id: string) => h.users.get(id) ?? null,
|
||||
findById: async (id: string) => withIdentities(h.users.get(id)),
|
||||
findByUsername: async (username: string) =>
|
||||
[...h.users.values()].find((user) => user.username === username) ?? null,
|
||||
update: async (id: string, changes: Partial<User>) => {
|
||||
Object.assign(h.users.get(id)!, changes);
|
||||
},
|
||||
withIdentities(
|
||||
[...h.users.values()].find((user) => user.username === username),
|
||||
),
|
||||
}),
|
||||
createCurrentUserAuthRepository: () => ({
|
||||
listIdentitiesForUser: async (userId: string) =>
|
||||
@@ -84,7 +88,6 @@ beforeEach(async () => {
|
||||
id: "admin",
|
||||
username: "admin",
|
||||
isAdmin: true,
|
||||
isOidc: false,
|
||||
passwordHash: "x",
|
||||
},
|
||||
],
|
||||
@@ -94,9 +97,7 @@ beforeEach(async () => {
|
||||
id: "sso",
|
||||
username: "alice-sso",
|
||||
isAdmin: false,
|
||||
isOidc: true,
|
||||
passwordHash: "",
|
||||
oidcIdentifier: "ldap:4:alice",
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -105,7 +106,6 @@ beforeEach(async () => {
|
||||
id: "local",
|
||||
username: "alice",
|
||||
isAdmin: false,
|
||||
isOidc: false,
|
||||
passwordHash: "hash",
|
||||
},
|
||||
],
|
||||
@@ -149,16 +149,12 @@ function post(path: string, body: unknown, user = "admin") {
|
||||
}
|
||||
|
||||
describe("linking an external account to a password account", () => {
|
||||
it("moves the identities and the old identifier, then removes the external account", async () => {
|
||||
const response = await post("/link-oidc-to-password", {
|
||||
oidcUserId: "sso",
|
||||
it("moves the identities, then removes the external account", async () => {
|
||||
const response = await post("/link-external-to-password", {
|
||||
externalUserId: "sso",
|
||||
targetUsername: "alice",
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(h.users.get("local")).toMatchObject({
|
||||
isOidc: true,
|
||||
oidcIdentifier: "ldap:4:alice",
|
||||
});
|
||||
expect(h.identities).toEqual([
|
||||
{ userId: "local", providerId: "ldap:4", subject: "alice" },
|
||||
]);
|
||||
@@ -166,13 +162,13 @@ describe("linking an external account to a password account", () => {
|
||||
expect(h.deleted).toEqual(["sso"]);
|
||||
});
|
||||
|
||||
it("takes the new path and field name", async () => {
|
||||
const response = await post("/link-external-to-password", {
|
||||
externalUserId: "sso",
|
||||
it("no longer takes the 2.8 path and field name", async () => {
|
||||
const response = await post("/link-oidc-to-password", {
|
||||
oidcUserId: "sso",
|
||||
targetUsername: "alice",
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(h.identities[0].userId).toBe("local");
|
||||
expect(response.status).toBe(404);
|
||||
expect(h.identities[0].userId).toBe("sso");
|
||||
});
|
||||
|
||||
it("refuses a target that already signs in externally", async () => {
|
||||
@@ -186,8 +182,8 @@ describe("linking an external account to a password account", () => {
|
||||
|
||||
it("is for admins only", async () => {
|
||||
const response = await post(
|
||||
"/link-oidc-to-password",
|
||||
{ oidcUserId: "sso", targetUsername: "alice" },
|
||||
"/link-external-to-password",
|
||||
{ externalUserId: "sso", targetUsername: "alice" },
|
||||
"local",
|
||||
);
|
||||
expect(response.status).toBe(403);
|
||||
@@ -195,8 +191,8 @@ describe("linking an external account to a password account", () => {
|
||||
});
|
||||
|
||||
it("refuses a target that is not a password account", async () => {
|
||||
const response = await post("/link-oidc-to-password", {
|
||||
oidcUserId: "sso",
|
||||
const response = await post("/link-external-to-password", {
|
||||
externalUserId: "sso",
|
||||
targetUsername: "alice-sso",
|
||||
});
|
||||
expect(response.status).toBe(400);
|
||||
@@ -207,17 +203,13 @@ describe("linking an external account to a password account", () => {
|
||||
id: "orig",
|
||||
username: "bob",
|
||||
isAdmin: false,
|
||||
isOidc: true,
|
||||
passwordHash: "",
|
||||
oidcIdentifier: "sub-9",
|
||||
});
|
||||
h.users.set("dup", {
|
||||
id: "dup",
|
||||
username: "bob-1",
|
||||
isAdmin: false,
|
||||
isOidc: true,
|
||||
passwordHash: "",
|
||||
oidcIdentifier: null,
|
||||
});
|
||||
h.identities.push(
|
||||
{ userId: "orig", providerId: "legacy-oidc", subject: "sub-9" },
|
||||
@@ -231,7 +223,6 @@ describe("linking an external account to a password account", () => {
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(h.deleted).toEqual(["dup"]);
|
||||
expect(h.users.get("orig")!.oidcIdentifier).toBe("sub-9");
|
||||
expect(
|
||||
h.identities
|
||||
.filter((row) => row.userId === "orig")
|
||||
@@ -244,9 +235,7 @@ describe("linking an external account to a password account", () => {
|
||||
id: "other",
|
||||
username: "carol",
|
||||
isAdmin: false,
|
||||
isOidc: true,
|
||||
passwordHash: "",
|
||||
oidcIdentifier: "sub-x",
|
||||
});
|
||||
h.identities.push({
|
||||
userId: "other",
|
||||
@@ -264,22 +253,6 @@ describe("linking an external account to a password account", () => {
|
||||
|
||||
describe("unlinking", () => {
|
||||
it("drops the identities so the provider no longer signs in as the user", async () => {
|
||||
await post("/link-oidc-to-password", {
|
||||
oidcUserId: "sso",
|
||||
targetUsername: "alice",
|
||||
});
|
||||
const response = await post("/unlink-oidc-from-password", {
|
||||
userId: "local",
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(h.users.get("local")).toMatchObject({
|
||||
isOidc: false,
|
||||
oidcIdentifier: null,
|
||||
});
|
||||
expect(h.identities).toEqual([]);
|
||||
});
|
||||
|
||||
it("takes the new path", async () => {
|
||||
await post("/link-external-to-password", {
|
||||
externalUserId: "sso",
|
||||
targetUsername: "alice",
|
||||
@@ -291,8 +264,15 @@ describe("unlinking", () => {
|
||||
expect(h.identities).toEqual([]);
|
||||
});
|
||||
|
||||
it("refuses to leave an account with no way in", async () => {
|
||||
it("no longer takes the 2.8 path", async () => {
|
||||
const response = await post("/unlink-oidc-from-password", {
|
||||
userId: "local",
|
||||
});
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it("refuses to leave an account with no way in", async () => {
|
||||
const response = await post("/unlink-external-from-password", {
|
||||
userId: "sso",
|
||||
});
|
||||
expect(response.status).toBe(400);
|
||||
|
||||
Reference in new issue
Block a user