Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions electron/src/auth/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,10 +498,11 @@ export class DesktopAuthService {
}
}

async createKokoConnectTicket({ baseUrl, tokenId }) {
async createKokoConnectTicket({ baseUrl, tokenId, orgId = "" }) {
const session = this.currentSession();
const bearer = await this.freshToken(session.origin, session.sessionKey, session.bearerToken);
session.bearerToken = bearer;
const ticketOrgId = orgId || session.orgId || "";

const base = parseUrl(baseUrl);
if (!["http:", "https:"].includes(base.protocol) || !base.hostname || base.username || base.password) {
Expand All @@ -515,11 +516,11 @@ export class DesktopAuthService {
Accept: "application/json",
Authorization: `Bearer ${bearer}`,
"Content-Type": "application/json",
"X-JMS-ORG": session.orgId || "",
"X-JMS-ORG": ticketOrgId,
"X-TZ": timezoneOffset(),
Referer: url.origin
},
body: JSON.stringify({ token_id: tokenId, org_id: session.orgId || "" })
body: JSON.stringify({ token_id: tokenId, org_id: ticketOrgId })
});
const text = await response.text();
if (response.status !== 201) {
Expand Down
25 changes: 20 additions & 5 deletions electron/tests/auth-request-site.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,48 @@ import test from "node:test";
import { runInNewContext } from "node:vm";
import { parseUrl } from "../src/shared/url.ts";

// Exercise both request paths without starting Electron, like the auth-language checks.
// Exercise request paths without starting Electron, like the auth-language checks.
const source = readFileSync(new URL("../src/auth/service.ts", import.meta.url), "utf8");
const helpers = source.slice(source.indexOf("function endpoint("), source.indexOf("function base64Url("));
const methods = source.slice(source.indexOf(" async apiRequest("), source.indexOf(" async createKokoConnectTicket("));
const methods = source.slice(source.indexOf(" async apiRequest("), source.indexOf(" async logout("));
const script = stripTypeScriptTypes(`${helpers}\nnew (class { ${methods} })()`);

function setup(env: Record<string, string> = {}, origin = "https://jumpserver.test") {
function setup(env: Record<string, string> = {}, origin = "https://jumpserver.test", responseStatus = 200) {
const requests: { url: string; init: RequestInit }[] = [];
const session = { origin, sessionKey: "site", bearerToken: "test-token", orgId: "test-org" };
const auth = runInNewContext(script, {
process: { env },
parseUrl,
URL,
AbortSignal,
TextDecoder,
electronLog: { warn() {} }
electronLog: { warn() {}, info() {} }
});
Object.assign(auth, {
currentSession: () => session,
freshToken: async () => session.bearerToken,
fetchSite: async (url: string, init: RequestInit) => {
requests.push({ url, init });
return new Response("{}");
return new Response("{}", { status: responseStatus });
}
});
return { auth, requests, session };
}

test("Koko tickets bind to the token organization without changing the selected organization", async () => {
for (const orgId of ["asset-org", undefined]) {
const { auth, requests, session } = setup({}, "https://jumpserver.test", 201);
session.orgId = "00000000-0000-0000-0000-000000000000";
await auth.createKokoConnectTicket({ baseUrl: "https://koko.test/site/", tokenId: "token-id", orgId });
const { url, init } = requests[0];
assert.equal(url, "https://koko.test/site/koko/api/connect-ticket/");
assert.equal(init.headers["Authorization"], "Bearer test-token");
assert.equal(init.headers["X-JMS-ORG"], orgId || session.orgId);
assert.deepEqual(JSON.parse(String(init.body)), { token_id: "token-id", org_id: orgId || session.orgId });
assert.equal(session.orgId, "00000000-0000-0000-0000-000000000000");
}
});

for (const method of ["apiRequest", "apiStreamRequest"]) {
const request = { method: "GET", service: "kael", path: "/kael/api/v1/bootstrap" };

Expand Down
7 changes: 6 additions & 1 deletion ui/composables/useAssetAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ describe("opening assets in local applications", () => {
[undefined, false],
[15001, undefined]
])("uses the Web Proxy endpoint port %s with license %s", async (port, license) => {
mocks.createToken.mockResolvedValue({ id: "id", value: "token-value", org_id: "asset-org" });
mocks.getPublicSettings.mockResolvedValue({ XPACK_LICENSE_IS_VALID: license });
vi.stubGlobal("isDesktopRuntime", () => true);
vi.stubGlobal("isElectronRuntime", () => true);
Expand Down Expand Up @@ -655,7 +656,11 @@ describe("opening assets in local applications", () => {
});
await vi.waitFor(() => expect(ready.mock.calls.length + failed.mock.calls.length).toBe(1));
expect(failed).not.toHaveBeenCalled();
expect(mocks.createTicket).toHaveBeenCalledWith({ baseUrl: "https://proxy.example", tokenId: "id" });
expect(mocks.createTicket).toHaveBeenCalledWith({
baseUrl: "https://proxy.example",
tokenId: "id",
orgId: "asset-org"
});
expect(ready.mock.calls[0]?.[0].webProxy.ticket).toBe("web-ticket");
expect(ready.mock.calls[0]?.[0].webProxy.ticketEndpoint).toBe(mocks.createTicket.mock.calls[0]?.[0].baseUrl);
expect(ready.mock.calls[0]?.[0].webProxy.recordingEnabled).toBe(license === true);
Expand Down
3 changes: 2 additions & 1 deletion ui/composables/useAssetAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,8 @@ export const useAssetAction = () => {
: endpointUrl;
const { ticket } = await useWorkspaceConnectors().createKokoTicket({
baseUrl: ticketEndpoint,
tokenId: token.id
tokenId: token.id,
orgId: token.org_id
});
if (!ticket) throw new Error("Koko 未返回 Web Proxy connect ticket");
const settings = await getPublicSettings();
Expand Down
17 changes: 10 additions & 7 deletions ui/composables/useWorkspaceConnectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ afterEach(() => {
useWorkspaceConnectors().registerKokoTicketProvider(null);
});

it("creates a desktop ticket without mounting the workspace layout", async () => {
mocks.desktop = true;
mocks.invoke.mockResolvedValue({ ticket: "desktop-ticket" });
const request = { baseUrl: "https://endpoint.example", tokenId: "" };
await expect(useWorkspaceConnectors().createKokoTicket(request)).resolves.toEqual({ ticket: "desktop-ticket" });
expect(mocks.invoke).toHaveBeenCalledWith("create_koko_connect_ticket", request);
});
it.each([undefined, "asset-org"])(
"creates a desktop ticket with organization %s without the workspace layout",
async (orgId) => {
mocks.desktop = true;
mocks.invoke.mockResolvedValue({ ticket: "desktop-ticket" });
const request = { baseUrl: "https://endpoint.example", tokenId: "token", ...(orgId ? { orgId } : {}) };
await expect(useWorkspaceConnectors().createKokoTicket(request)).resolves.toEqual({ ticket: "desktop-ticket" });
expect(mocks.invoke).toHaveBeenCalledWith("create_koko_connect_ticket", request);
}
);

it("retains cookie authentication and explicit providers in the browser", async () => {
const connectors = useWorkspaceConnectors();
Expand Down
6 changes: 2 additions & 4 deletions ui/composables/useWorkspaceConnectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { isDesktopRuntime } from "~/utils/runtime";
export interface KokoTicketRequest {
baseUrl: string;
tokenId: string;
orgId?: string;
}

export interface KokoTicketResult {
Expand All @@ -22,10 +23,7 @@ export const useWorkspaceConnectors = () => {
const createKokoTicket = (request: KokoTicketRequest) => {
// Standalone connector pages do not mount the workspace ticket provider.
if (!kokoTicketProvider && isDesktopRuntime()) {
return desktopInvoke<KokoTicketResult>("create_koko_connect_ticket", {
baseUrl: request.baseUrl,
tokenId: request.tokenId
});
return desktopInvoke<KokoTicketResult>("create_koko_connect_ticket", { ...request });
}
if (!kokoTicketProvider) return Promise.resolve<KokoTicketResult>({});
return kokoTicketProvider(request);
Expand Down
8 changes: 6 additions & 2 deletions ui/composables/useWorkspaceTabMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function session(webProxy = true) {

beforeEach(() => {
vi.clearAllMocks();
mocks.exchange.mockResolvedValue({ id: "new-token", value: "new-value" });
mocks.exchange.mockResolvedValue({ id: "new-token", value: "new-value", org_id: "asset-org" });
createKokoTicket.mockResolvedValue({ ticket: "new-ticket" });
vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
vi.stubGlobal("useErrorToast", () => ({ addErrorToast }));
Expand All @@ -63,7 +63,11 @@ it.each(["reconnect", "clone", "pane"])("renews the token and bound ticket for %
else await menu.connectCurrentPane(tab, { id: "other-pane" } as never);

expect(mocks.exchange).toHaveBeenCalledWith("old-token");
expect(createKokoTicket).toHaveBeenCalledWith({ baseUrl: "https://koko.example", tokenId: "new-token" });
expect(createKokoTicket).toHaveBeenCalledWith({
baseUrl: "https://koko.example",
tokenId: "new-token",
orgId: "asset-org"
});
const payload = action === "clone" ? openSession.mock.calls[0]![1].payload : updateSessionPayload.mock.calls[0]![1];
expect(payload).toMatchObject({
id: "new-token",
Expand Down
3 changes: 2 additions & 1 deletion ui/composables/useWorkspaceTabMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ async function buildPayload(tab: Pick<WorkspaceSessionTab, "payload">, token: To
if (!webProxy.ticketEndpoint) throw new Error("missing Web Proxy ticket endpoint");
const { ticket } = await useWorkspaceConnectors().createKokoTicket({
baseUrl: webProxy.ticketEndpoint,
tokenId: token.id
tokenId: token.id,
orgId: token.org_id
});
if (!ticket) throw new Error("Koko 未返回 Web Proxy connect ticket");
webProxy = { ...webProxy, ticket };
Expand Down
5 changes: 1 addition & 4 deletions ui/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,7 @@ onMounted(() => {
registerSessionDisposer(() => {});
registerKokoTicketProvider(async (request) => {
if (isDesktopRuntime()) {
return desktopInvoke("create_koko_connect_ticket", {
baseUrl: request.baseUrl,
tokenId: request.tokenId
});
return desktopInvoke("create_koko_connect_ticket", { ...request });
}

const url = `${request.baseUrl.replace(/\/+$/, "")}/koko/api/connect-ticket/`;
Expand Down
5 changes: 1 addition & 4 deletions ui/pages/session/[assetId].vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,7 @@ onMounted(() => {
registerSessionDisposer(() => {});
registerKokoTicketProvider(async (request) => {
if (isDesktopRuntime()) {
return desktopInvoke("create_koko_connect_ticket", {
baseUrl: request.baseUrl,
tokenId: request.tokenId
});
return desktopInvoke("create_koko_connect_ticket", { ...request });
}

const url = `${request.baseUrl.replace(/\/+$/, "")}/koko/api/connect-ticket/`;
Expand Down
Loading