Skip to content

Commit 81d4e81

Browse files
fjakobspietern
andauthored
Interactive AAD setup (#308)
Logging in with `az login`. Including detecting the wrong tenant ID. https://user-images.githubusercontent.com/40952/207889437-84b329c1-14d5-45b7-a9f8-1c96884ab79f.mov Instructions for installing az CLI: https://user-images.githubusercontent.com/40952/207889556-36ffd664-cd78-420d-aa37-e5fbac060e92.mov Co-authored-by: Pieter Noordhuis <[email protected]>
1 parent 3d692f2 commit 81d4e81

File tree

9 files changed

+63
-58
lines changed

9 files changed

+63
-58
lines changed

packages/databricks-sdk-js/src/api-client.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe(__filename, () => {
1212
});
1313

1414
it("should create proper user agent", () => {
15-
const ua = new ApiClient("unit", "3.4.5").userAgent();
15+
const ua = new ApiClient({extraUserAgent: {unit: "3.4.5"}}).userAgent();
1616
assert.equal(
1717
ua,
1818
`unit/3.4.5 databricks-sdk-js/${sdkVersion} nodejs/${process.version.slice(

packages/databricks-sdk-js/src/api-client.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {context} from "./context";
99
import {Context} from "./context";
1010
import retry, {RetriableError} from "./retries/retries";
1111
import Time, {TimeUnits} from "./retries/Time";
12+
import {CredentialProvider} from "./auth/types";
1213

1314
// eslint-disable-next-line @typescript-eslint/no-var-requires
1415
const sdkVersion = require("../package.json").version;
@@ -46,6 +47,9 @@ export class ApiClient {
4647
private agent: https.Agent;
4748
private _host?: URL;
4849

50+
private credentialProvider: CredentialProvider;
51+
private readonly extraUserAgent: Record<string, string>;
52+
4953
get host(): Promise<URL> {
5054
return (async () => {
5155
if (!this._host) {
@@ -56,24 +60,34 @@ export class ApiClient {
5660
})();
5761
}
5862

59-
constructor(
60-
private readonly product: string,
61-
private readonly productVersion: string,
62-
private credentialProvider = fromDefaultChain
63-
) {
63+
constructor({
64+
credentialProvider = fromDefaultChain,
65+
extraUserAgent = {},
66+
}: {
67+
credentialProvider?: CredentialProvider;
68+
extraUserAgent?: Record<string, string>;
69+
}) {
70+
this.credentialProvider = credentialProvider;
71+
this.extraUserAgent = extraUserAgent;
72+
6473
this.agent = new https.Agent({
6574
keepAlive: true,
6675
keepAliveMsecs: 15_000,
6776
});
6877
}
6978

7079
userAgent(): string {
71-
const pairs = [
72-
`${this.product}/${this.productVersion}`,
80+
const pairs: Array<string> = [];
81+
for (const [key, value] of Object.entries(this.extraUserAgent)) {
82+
pairs.push(`${key}/${value}`);
83+
}
84+
85+
pairs.push(
7386
`databricks-sdk-js/${sdkVersion}`,
7487
`nodejs/${process.version.slice(1)}`,
75-
`os/${process.platform}`,
76-
];
88+
`os/${process.platform}`
89+
);
90+
7791
// TODO: add ability of per-request extra-information,
7892
// so that we can track sub-functionality, like in Terraform
7993
return pairs.join(" ");
@@ -165,7 +179,7 @@ export class ApiClient {
165179

166180
// throw error if the URL is incorrect and we get back an HTML page
167181
if (response.headers.get("content-type")?.match("text/html")) {
168-
// When the AAD tenent is not configured correctly, the response is a HTML page with a title like this:
182+
// When the AAD tenant is not configured correctly, the response is a HTML page with a title like this:
169183
// "Error 400 io.jsonwebtoken.IncorrectClaimException: Expected iss claim to be: https://sts.windows.net/aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaa/, but was: https://sts.windows.net/bbbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbb/."
170184
const m = responseText.match(/<title>(Error \d+.*?)<\/title>/);
171185
let error: HttpError;

packages/databricks-sdk-js/src/auth/fromAzureCli.integ.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ describe.skip(__filename, function () {
88
this.timeout(15_000);
99

1010
it("should login with Azure CLI", async () => {
11-
const client = new ApiClient("test", "0.1", fromAzureCli());
11+
const client = new ApiClient({
12+
credentialProvider: fromAzureCli(),
13+
});
1214

1315
const scimApi = new CurrentUserService(client);
1416
await scimApi.me();

packages/databricks-sdk-js/src/test/IntegrationTestSetup.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ export class IntegrationTestSetup {
1414
private static _instance: IntegrationTestSetup;
1515
static async getInstance(): Promise<IntegrationTestSetup> {
1616
if (!this._instance) {
17-
const client = new ApiClient("integration-tests", "0.0.1");
17+
const client = new ApiClient({
18+
extraUserAgent: {"integration-tests": "0.0.1"},
19+
});
1820

1921
if (!process.env["TEST_DEFAULT_CLUSTER_ID"]) {
2022
throw new Error(

packages/databricks-vscode/package.json

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,7 @@
3737
"url": "https://github.com/databricks/databricks-vscode.git"
3838
},
3939
"activationEvents": [
40-
"onCommand:databricks.connection.logout",
41-
"onCommand:databricks.connection.configureWorkspace",
42-
"onCommand:databricks.connection.openDatabricksConfigFile",
43-
"onCommand:databricks.connection.attachCluster",
44-
"onCommand:databricks.connection.attachClusterQuickPick",
45-
"onCommand:databricks.connection.detachCluster",
46-
"onCommand:databricks.connection.attachSyncDestination",
47-
"onCommand:databricks.connection.detachSyncDestination",
48-
"onCommand:databricks.sync.start",
49-
"onCommand:databricks.sync.startFull",
50-
"onCommand:databricks.sync.stop",
51-
"onCommand:databricks.cluster.refresh",
52-
"onCommand:databricks.cluster.filterByAll",
53-
"onCommand:databricks.cluster.filterByMe",
54-
"onCommand:databricks.cluster.filterByRunning",
55-
"onCommand:databricks.run.getProgramName",
56-
"onCommand:databricks.run.runEditorContentsAsWorkflow",
57-
"onCommand:databricks.run.runEditorContents",
58-
"onCommand:databricks.quickstart.open",
59-
"onCommand:databricks.logs.openFolder",
60-
"onCommand:databricks.autocomplete.configure",
40+
"onCommand:databricks.*",
6141
"onView:configurationView",
6242
"onView:clusterView",
6343
"onTaskType:databricks",

packages/databricks-vscode/src/configuration/AuthProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,6 @@ export class AzureCliAuthProvider extends AuthProvider {
171171
}
172172

173173
async check(silent: boolean): Promise<boolean> {
174-
return await new AzureCliCheck().check(silent);
174+
return await new AzureCliCheck(this).check(silent);
175175
}
176176
}

packages/databricks-vscode/src/configuration/AzureCliCheck.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as child_process from "node:child_process";
22
import {promisify} from "node:util";
3-
import {CurrentUserService, fromAzureCli} from "@databricks/databricks-sdk";
3+
import {CurrentUserService} from "@databricks/databricks-sdk";
44
import {commands, Disposable, Uri, window} from "vscode";
55
import {ConnectionManager} from "./ConnectionManager";
6+
import {AuthProvider} from "./AuthProvider";
67

78
const execFile = promisify(child_process.execFile);
89

@@ -59,24 +60,22 @@ type AzureStepName =
5960
export class AzureCliCheck implements Disposable {
6061
private disposables: Disposable[] = [];
6162

62-
constructor(private azBinPath: string = "az") {}
63+
constructor(
64+
private authProvider: AuthProvider,
65+
private azBinPath: string = "az"
66+
) {}
6367

6468
dispose() {
6569
this.disposables.forEach((i) => i.dispose());
6670
this.disposables = [];
6771
}
6872

69-
public async check(
70-
silent = false,
71-
host: URL = new URL(
72-
"https://adb-309687753508875.15.azuredatabricks.net"
73-
)
74-
): Promise<boolean> {
73+
public async check(silent = false): Promise<boolean> {
7574
let tenant: string;
7675

7776
const steps: Record<AzureStepName, Step<boolean, AzureStepName>> = {
7877
tryLogin: async () => {
79-
const result = await this.tryLogin(host);
78+
const result = await this.tryLogin();
8079
if (typeof result === "string") {
8180
tenant = result;
8281
return {
@@ -162,8 +161,8 @@ export class AzureCliCheck implements Disposable {
162161
return result;
163162
}
164163

165-
private async tryLogin(host: URL): Promise<boolean | string> {
166-
const apiClient = ConnectionManager.apiClientFrom(fromAzureCli(host));
164+
private async tryLogin(): Promise<boolean | string> {
165+
const apiClient = ConnectionManager.apiClientFrom(this.authProvider);
167166
try {
168167
await new CurrentUserService(apiClient).me();
169168
} catch (e: any) {

packages/databricks-vscode/src/configuration/ConnectionManager.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
ApiClient,
33
Cluster,
4-
CredentialProvider,
54
WorkspaceService,
65
HttpError,
76
} from "@databricks/databricks-sdk";
@@ -15,12 +14,13 @@ import {
1514
import {CliWrapper} from "../cli/CliWrapper";
1615
import {SyncDestination} from "./SyncDestination";
1716
import {ProjectConfig, ProjectConfigFile} from "./ProjectConfigFile";
18-
import {configureWorkspaceWizard} from "./selectProfileWizard";
17+
import {configureWorkspaceWizard} from "./configureWorkspaceWizard";
1918
import {ClusterManager} from "../cluster/ClusterManager";
2019
import {workspace} from "@databricks/databricks-sdk";
2120
import {DatabricksWorkspace} from "./DatabricksWorkspace";
2221
import {NamedLogger} from "@databricks/databricks-sdk/dist/logging";
2322
import {Loggers} from "../logger";
23+
import {AuthProvider} from "./AuthProvider";
2424

2525
// eslint-disable-next-line @typescript-eslint/no-var-requires
2626
const extensionVersion = require("../../package.json").version;
@@ -90,8 +90,16 @@ export class ConnectionManager {
9090
return this._apiClient;
9191
}
9292

93-
public static apiClientFrom(creds: CredentialProvider): ApiClient {
94-
return new ApiClient("vscode-extension", extensionVersion, creds);
93+
public static apiClientFrom(authProvider: AuthProvider): ApiClient {
94+
return new ApiClient({
95+
extraUserAgent: {
96+
// eslint-disable-next-line @typescript-eslint/naming-convention
97+
"vscode-extension": extensionVersion,
98+
// eslint-disable-next-line @typescript-eslint/naming-convention
99+
"auth-type": authProvider.authType,
100+
},
101+
credentialProvider: authProvider.getCredentialProvider(),
102+
});
95103
}
96104

97105
async login(interactive = false): Promise<void> {
@@ -118,18 +126,17 @@ export class ConnectionManager {
118126
vscodeWorkspace.rootPath
119127
);
120128

121-
const credentialProvider =
122-
projectConfigFile.authProvider.getCredentialProvider();
123-
124129
if (!(await projectConfigFile.authProvider.check(true))) {
125130
throw new Error(
126131
`Can't login with ${projectConfigFile.authProvider.describe()}.`
127132
);
128133
}
129134

130-
await credentialProvider();
135+
await projectConfigFile.authProvider.getCredentialProvider();
131136

132-
apiClient = ConnectionManager.apiClientFrom(credentialProvider);
137+
apiClient = ConnectionManager.apiClientFrom(
138+
projectConfigFile.authProvider
139+
);
133140
this._databricksWorkspace = await DatabricksWorkspace.load(
134141
apiClient,
135142
projectConfigFile.authProvider
@@ -238,12 +245,13 @@ export class ConnectionManager {
238245
return;
239246
}
240247

241-
const credentialProvider =
242-
config.authProvider.getCredentialProvider();
248+
if (!(await config.authProvider.check(false))) {
249+
return;
250+
}
243251

244252
try {
245253
await DatabricksWorkspace.load(
246-
ConnectionManager.apiClientFrom(credentialProvider),
254+
ConnectionManager.apiClientFrom(config.authProvider),
247255
config.authProvider
248256
);
249257
} catch (e: any) {

0 commit comments

Comments
 (0)