Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KXI-46026: Support Insights Enterprise Trial (add realm field) #339

Merged
merged 10 commits into from
Jun 7, 2024
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
6 changes: 6 additions & 0 deletions src/classes/insightsConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class InsightsConnection {
await getCurrentToken(
this.node.details.server,
this.node.details.alias,
this.node.details.realm || "insights",
).then(async (token) => {
this.connected = token ? true : false;
if (token) {
Expand Down Expand Up @@ -77,6 +78,7 @@ export class InsightsConnection {
const token = await getCurrentToken(
this.node.details.server,
this.node.details.alias,
this.node.details.realm || "insights",
);

if (token === undefined) {
Expand Down Expand Up @@ -108,6 +110,7 @@ export class InsightsConnection {
const token = await getCurrentToken(
this.node.details.server,
this.node.details.alias,
this.node.details.realm || "insights",
);
if (token === undefined) {
tokenUndefinedError(this.connLabel);
Expand Down Expand Up @@ -213,6 +216,7 @@ export class InsightsConnection {
const token = await getCurrentToken(
this.node.details.server,
this.node.details.alias,
this.node.details.realm || "insights",
);

if (token === undefined) {
Expand Down Expand Up @@ -289,6 +293,7 @@ export class InsightsConnection {
const token = await getCurrentToken(
this.node.details.server,
this.node.details.alias,
this.node.details.realm || "insights",
);
if (token === undefined) {
tokenUndefinedError(this.connLabel);
Expand Down Expand Up @@ -357,6 +362,7 @@ export class InsightsConnection {
const token = await getCurrentToken(
this.node.details.server,
this.node.details.alias,
this.node.details.realm || "insights",
);

if (token === undefined) {
Expand Down
2 changes: 2 additions & 0 deletions src/commands/serverCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,15 @@ export async function addInsightsConnection(insightsData: InsightDetails) {
auth: true,
alias: insightsData.alias,
server: insightsData.server!,
realm: insightsData.realm,
},
};
} else {
insights[key] = {
auth: true,
alias: insightsData.alias,
server: insightsData.server!,
realm: insightsData.realm,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/models/insights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface InsightDetails {
alias: string;
server: string;
auth: boolean;
realm?: string;
}

export interface Insights {
Expand Down
51 changes: 38 additions & 13 deletions src/services/kdbInsights/codeFlowLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ interface IDeferred<T> {
reject: (reason: any) => void;
}

function getAuthUrl(insightsUrl: string, realm: string) {
return new url.URL(
`auth/realms/${realm}/protocol/openid-connect/auth`,
insightsUrl,
);
}

function getTokenUrl(insightsUrl: string, realm: string) {
return new url.URL(
`auth/realms/${realm}/protocol/openid-connect/token`,
insightsUrl,
);
}

function getRevokeUrl(insightsUrl: string, realm: string) {
return new url.URL(
`auth/realms/${realm}/protocol/openid-connect/revoke`,
insightsUrl,
);
}

export interface IToken {
accessToken: string;
accessTokenExpirationDate: Date;
Expand All @@ -41,7 +62,7 @@ const commonRequestParams = {
client_id: "insights-app",
};

export async function signIn(insightsUrl: string) {
export async function signIn(insightsUrl: string, realm: string) {
const { server, codePromise } = createServer();

try {
Expand All @@ -54,24 +75,22 @@ export async function signIn(insightsUrl: string) {
state: crypto.randomBytes(20).toString("hex"),
};

const authorizationUrl = new url.URL(
ext.insightsAuthUrls.authURL,
insightsUrl,
);
const authorizationUrl = getAuthUrl(insightsUrl, realm);

authorizationUrl.search = queryString(authParams);

await env.openExternal(Uri.parse(authorizationUrl.toString()));

const code = await codePromise;
return await getToken(insightsUrl, code);
return await getToken(insightsUrl, realm, code);
} finally {
setImmediate(() => server.close());
}
}

export async function signOut(
insightsUrl: string,
realm: string,
token: string,
): Promise<void> {
const queryParams = queryString({
Expand All @@ -84,7 +103,7 @@ export async function signOut(
const headers = {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
};
const requestUrl = new url.URL(ext.insightsAuthUrls.revoke, insightsUrl);
const requestUrl = getRevokeUrl(insightsUrl, realm);

await axios.post(requestUrl.toString(), body, headers).then((res) => {
return res.data;
Expand All @@ -93,17 +112,20 @@ export async function signOut(

export async function refreshToken(
insightsUrl: string,
realm: string,
token: string,
): Promise<IToken | undefined> {
return await tokenRequest(insightsUrl, {
return await tokenRequest(insightsUrl, realm, {
grant_type: ext.insightsGrantType.refreshToken,
refresh_token: token,
});
}

/* istanbul ignore next */
export async function getCurrentToken(
serverName: string,
serverAlias: string,
realm: string,
): Promise<IToken | undefined> {
if (serverName === "" || serverAlias === "") {
return undefined;
Expand All @@ -115,9 +137,9 @@ export async function getCurrentToken(
if (existingToken !== undefined) {
const storedToken: IToken = JSON.parse(existingToken);
if (new Date(storedToken.accessTokenExpirationDate) < new Date()) {
token = await refreshToken(serverName, storedToken.refreshToken);
token = await refreshToken(serverName, realm, storedToken.refreshToken);
if (token === undefined) {
token = await signIn(serverName);
token = await signIn(serverName, realm);
ext.context.secrets.store(serverAlias, JSON.stringify(token));
}
ext.context.secrets.store(serverAlias, JSON.stringify(token));
Expand All @@ -126,24 +148,27 @@ export async function getCurrentToken(
return storedToken;
}
} else {
token = await signIn(serverName);
token = await signIn(serverName, realm);
ext.context.secrets.store(serverAlias, JSON.stringify(token));
}
return token;
}

/* istanbul ignore next */
async function getToken(
insightsUrl: string,
realm: string,
code: string,
): Promise<IToken | undefined> {
return await tokenRequest(insightsUrl, {
return await tokenRequest(insightsUrl, realm, {
code,
grant_type: ext.insightsGrantType.authorizationCode,
});
}

async function tokenRequest(
insightsUrl: string,
realm: string,
params: any,
): Promise<IToken | undefined> {
const queryParams = queryString(params);
Expand All @@ -155,7 +180,7 @@ async function tokenRequest(
signal: AbortSignal.timeout(closeTimeout),
};

const requestUrl = new url.URL(ext.insightsAuthUrls.tokenURL, insightsUrl);
const requestUrl = getTokenUrl(insightsUrl, realm);

let response;
if (params.grant_type === "refresh_token") {
Expand Down
32 changes: 32 additions & 0 deletions src/webview/components/kdbNewConnectionView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class KdbNewConnectionView extends LitElement {
alias: "",
server: "",
auth: true,
realm: "",
};
this.bundledServer = {
serverName: "127.0.0.1",
Expand Down Expand Up @@ -220,6 +221,29 @@ export class KdbNewConnectionView extends LitElement {
`;
}

renderRealm() {
return html`
<div class="row mt-1">
<vscode-text-field
class="text-field larger option-title"
value="${this.insightsServer.realm}"
placeholder="insights"
@input="${(event: Event) => {
/* istanbul ignore next */
const value = (event.target as HTMLSelectElement).value;
/* istanbul ignore next */
this.insightsServer.realm = value;
}}"
>Define Realm (optional)</vscode-text-field
>
</div>
<div class="row option-description option-help">
Specify the Keycloak realm for authentication. Use this field to connect
to a specific realm as configured on your server.
</div>
`;
}

tabClickAction(tabNumber: number) {
const config =
this.tabConfig[tabNumber as keyof typeof this.tabConfig] ||
Expand Down Expand Up @@ -381,6 +405,14 @@ export class KdbNewConnectionView extends LitElement {
${this.renderConnAddress(ServerType.INSIGHTS)}
</div>
</div>
<div class="row">
<div class="col gap-0">
<details>
<summary>Advanced</summary>
${this.renderRealm()}
</details>
</div>
</div>
</div>
</vscode-panel-view>
</vscode-panels>
Expand Down
17 changes: 10 additions & 7 deletions test/suite/services.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,20 +560,21 @@ describe("Code flow login service tests", () => {

it("Should return a correct login", async () => {
sinon.stub(codeFlow, "signIn").returns(token);
const result = await signIn("http://localhost");
const result = await signIn("http://localhost", "insights");
assert.strictEqual(result, token, "Invalid token returned");
});

it("Should execute a correct logout", async () => {
sinon.stub(axios, "post").resolves(Promise.resolve({ data: token }));
const result = await signOut("http://localhost", "token");
const result = await signOut("http://localhost", "insights", "token");
assert.strictEqual(result, undefined, "Invalid response from logout");
});

it("Should execute token refresh", async () => {
sinon.stub(axios, "post").resolves(Promise.resolve({ data: token }));
const result = await refreshToken(
"http://localhost",
"insights",
JSON.stringify(token),
);
assert.strictEqual(
Expand All @@ -584,7 +585,7 @@ describe("Code flow login service tests", () => {
});

it("Should not return token from secret store", async () => {
const result = await getCurrentToken("", "testalias");
const result = await getCurrentToken("", "testalias", "insights");
assert.strictEqual(
result,
undefined,
Expand All @@ -593,17 +594,19 @@ describe("Code flow login service tests", () => {
});

it("Should not return token from secret store", async () => {
const result = await getCurrentToken("testserver", "");
const result = await getCurrentToken("testserver", "", "insights");
assert.strictEqual(
result,
undefined,
"Should return undefined when server alias is empty.",
);
});

it.skip("Should not sign in if link is not opened", async () => {
sinon.stub(env, "openExternal").value(async () => false);
await assert.rejects(() => signIn("http://127.0.0.1"));
it("Should continue sign in if link is copied", async () => {
sinon.stub(env, "openExternal").value(async () => {
throw new Error();
});
await assert.rejects(() => signIn("http://127.0.0.1", "insights"));
});
});

Expand Down
1 change: 1 addition & 0 deletions test/suite/webview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ describe("KdbNewConnectionView", () => {
alias: "",
server: "",
auth: true,
realm: "",
};
const data = view["data"];
assert.deepEqual(data, expectedData);
Expand Down
Loading