Skip to content

Commit 8a7dbda

Browse files
authored
feat: adds auth to vscode extension (#4188)
1 parent f28341a commit 8a7dbda

8 files changed

Lines changed: 347 additions & 11 deletions

File tree

vscode/extension/package-lock.json

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vscode/extension/package.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
],
2020
"main": "./dist/extension.js",
2121
"contributes": {
22+
"authentication": [
23+
{
24+
"id": "tobikodata",
25+
"label": "Tobiko"
26+
}
27+
],
2228
"commands": [
2329
{
2430
"command": "sqlmesh.format",
@@ -29,6 +35,16 @@
2935
"command": "sqlmesh.restart",
3036
"title": "Restart SQLMesh Servers",
3137
"description": "SQLMesh"
38+
},
39+
{
40+
"command": "sqlmesh.signin",
41+
"title": "Sign in to Tobiko Cloud",
42+
"description": "SQLMesh"
43+
},
44+
{
45+
"command": "sqlmesh.signout",
46+
"title": "Sign out from Tobiko Cloud",
47+
"description": "SQLMesh"
3248
}
3349
]
3450
},
@@ -52,7 +68,8 @@
5268
"@types/fs-extra": "^11.0.4",
5369
"@vscode/python-extension": "^1.0.5",
5470
"fs-extra": "^11.3.0",
55-
"vscode-languageclient": "^9.0.1"
71+
"vscode-languageclient": "^9.0.1",
72+
"zod": "^3.24.3"
5673
},
5774
"devDependencies": {
5875
"@types/mocha": "^10.0.10",

vscode/extension/src/auth/auth.ts

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import {
2+
env,
3+
Uri,
4+
AuthenticationProvider,
5+
AuthenticationProviderAuthenticationSessionsChangeEvent,
6+
AuthenticationSession,
7+
Event,
8+
EventEmitter,
9+
window,
10+
} from "vscode"
11+
import { get_tcloud_bin } from "../utilities/sqlmesh/sqlmesh"
12+
import { err, isErr, ok, Result } from "../utilities/functional/result"
13+
import { execAsync } from "../utilities/exec"
14+
import { getProjectRoot } from "../utilities/common/utilities"
15+
import z from "zod"
16+
import { traceError } from "../utilities/common/log"
17+
18+
export const AUTH_TYPE = "tobikodata"
19+
export const AUTH_NAME = "Tobiko"
20+
21+
const tokenSchema = z.object({
22+
iss: z.string(),
23+
aud: z.string(),
24+
sub: z.string(),
25+
scope: z.string(),
26+
iat: z.number(),
27+
exp: z.number(),
28+
email: z.string(),
29+
})
30+
const statusResponseSchema = z.object({
31+
is_logged_in: z.boolean(),
32+
id_token: tokenSchema,
33+
})
34+
35+
type StatusResponse = z.infer<typeof statusResponseSchema>;
36+
37+
const loginUrlResponseSchema = z.object({
38+
url: z.string(),
39+
verifier_code: z.string(),
40+
})
41+
42+
export class AuthenticationProviderTobikoCloud
43+
implements AuthenticationProvider
44+
{
45+
static id = AUTH_TYPE
46+
static name = AUTH_NAME
47+
48+
private _sessionChangeEmitter =
49+
new EventEmitter<AuthenticationProviderAuthenticationSessionsChangeEvent>()
50+
51+
onDidChangeSessions: Event<AuthenticationProviderAuthenticationSessionsChangeEvent> =
52+
this._sessionChangeEmitter.event
53+
54+
/**
55+
* Get the status of the authentication provider from the cli
56+
* @returns true if the user is logged in with the id token, false otherwise
57+
*/
58+
private async get_status(): Promise<Result<StatusResponse, string>> {
59+
const workspacePath = await getProjectRoot()
60+
const tcloudBin = await get_tcloud_bin()
61+
if (isErr(tcloudBin)) {
62+
return err(tcloudBin.error)
63+
}
64+
const tcloudBinPath = tcloudBin.value
65+
const result = await execAsync(
66+
tcloudBinPath,
67+
["auth", "vscode", "status"],
68+
{
69+
cwd: workspacePath.uri.fsPath,
70+
}
71+
)
72+
if (result.exitCode !== 0) {
73+
return err("Failed to get tcloud auth status")
74+
}
75+
const status = result.stdout
76+
const statusToJson = JSON.parse(status)
77+
const statusResponse = statusResponseSchema.parse(statusToJson)
78+
return ok(statusResponse)
79+
}
80+
81+
async getSessions(): Promise<AuthenticationSession[]> {
82+
const status = await this.get_status()
83+
if (isErr(status)) {
84+
return []
85+
}
86+
const statusResponse = status.value
87+
if (!statusResponse.is_logged_in) {
88+
return []
89+
}
90+
const token = statusResponse.id_token
91+
const session = {
92+
id: token.email,
93+
account: {
94+
id: token.email,
95+
label: "Tobiko",
96+
},
97+
scopes: token.scope.split(" "),
98+
accessToken: "",
99+
}
100+
return [session]
101+
}
102+
103+
async createSession(): Promise<AuthenticationSession> {
104+
const workspacePath = await getProjectRoot()
105+
const tcloudBin = await get_tcloud_bin()
106+
if (isErr(tcloudBin)) {
107+
throw new Error("Failed to get tcloud bin")
108+
}
109+
const tcloudBinPath = tcloudBin.value
110+
const result = await execAsync(
111+
tcloudBinPath,
112+
["auth", "vscode", "login-url"],
113+
{
114+
cwd: workspacePath.uri.fsPath,
115+
}
116+
)
117+
if (result.exitCode !== 0) {
118+
throw new Error("Failed to get tcloud login url")
119+
}
120+
const resultToJson = JSON.parse(result.stdout)
121+
const urlCode = loginUrlResponseSchema.parse(resultToJson)
122+
const url = urlCode.url
123+
124+
const ac = new AbortController()
125+
const timeout = setTimeout(() => ac.abort(), 1000 * 60 * 5)
126+
const backgroundServerForLogin = execAsync(
127+
tcloudBinPath,
128+
["auth", "vscode", "start-server", urlCode.verifier_code],
129+
{
130+
cwd: workspacePath.uri.fsPath,
131+
signal: ac.signal,
132+
}
133+
)
134+
135+
const messageResult = await window.showInformationMessage(
136+
"Please login to Tobiko Cloud",
137+
{
138+
modal: true,
139+
},
140+
"Sign in with browser",
141+
"Cancel"
142+
)
143+
144+
if (messageResult === "Sign in with browser") {
145+
await env.openExternal(Uri.parse(url))
146+
}
147+
if (messageResult === "Cancel") {
148+
ac.abort()
149+
throw new Error("Login cancelled")
150+
}
151+
152+
try {
153+
const output = await backgroundServerForLogin
154+
if (output.exitCode !== 0) {
155+
throw new Error(`Failed to start server: ${output.stderr}`)
156+
}
157+
} catch (error) {
158+
traceError(`Server error: ${error}`)
159+
throw error
160+
}
161+
162+
clearTimeout(timeout)
163+
164+
const status = await this.get_status()
165+
if (isErr(status)) {
166+
throw new Error("Failed to get tcloud auth status")
167+
}
168+
const statusResponse = status.value
169+
if (!statusResponse.is_logged_in) {
170+
throw new Error("Failed to login to tcloud")
171+
}
172+
const scopes = statusResponse.id_token.scope.split(" ")
173+
const session: AuthenticationSession = {
174+
id: AuthenticationProviderTobikoCloud.id,
175+
account: {
176+
id: AuthenticationProviderTobikoCloud.id,
177+
label: "Tobiko",
178+
},
179+
scopes: scopes,
180+
accessToken: ""
181+
}
182+
return session
183+
}
184+
185+
async removeSession(): Promise<void> {
186+
const tcloudBin = await get_tcloud_bin()
187+
const workspacePath = await getProjectRoot()
188+
if (isErr(tcloudBin)) {
189+
throw new Error("Failed to get tcloud bin")
190+
}
191+
const tcloudBinPath = tcloudBin.value
192+
const result = await execAsync(tcloudBinPath, ["auth", "logout"], {
193+
cwd: workspacePath.uri.fsPath,
194+
})
195+
if (result.exitCode !== 0) {
196+
throw new Error("Failed to logout from tcloud")
197+
}
198+
}
199+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { AuthenticationProviderTobikoCloud } from "../auth/auth"
2+
import * as vscode from "vscode"
3+
4+
5+
export const signIn = (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => {
6+
await authenticationProvider.createSession()
7+
await vscode.window.showInformationMessage("Signed in successfully")
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { AuthenticationProviderTobikoCloud } from "../auth/auth"
2+
import * as vscode from "vscode"
3+
4+
export const signOut = (authenticationProvider: AuthenticationProviderTobikoCloud) => async () => {
5+
await authenticationProvider.removeSession()
6+
await vscode.window.showInformationMessage("Signed out successfully")
7+
}

vscode/extension/src/extension.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import {
1212
traceInfo,
1313
traceVerbose,
1414
} from "./utilities/common/log"
15-
import {
16-
onDidChangePythonInterpreter,
17-
} from "./utilities/common/python"
15+
import { onDidChangePythonInterpreter } from "./utilities/common/python"
1816
import { LSPClient } from "./lsp/lsp"
17+
import { AuthenticationProviderTobikoCloud } from "./auth/auth"
18+
import { signOut } from "./commands/signout"
19+
import { signIn } from "./commands/signin"
1920

2021
let lspClient: LSPClient | undefined
2122

@@ -29,17 +30,36 @@ export async function activate(context: vscode.ExtensionContext) {
2930
)
3031
traceInfo("Activating SQLMesh extension")
3132

32-
context.subscriptions.push(vscode.commands.registerCommand(
33-
"sqlmesh.format",
34-
async () => {
33+
traceInfo("Registering authentication provider")
34+
const authProvider = new AuthenticationProviderTobikoCloud()
35+
context.subscriptions.push(
36+
vscode.authentication.registerAuthenticationProvider(
37+
AuthenticationProviderTobikoCloud.id,
38+
AuthenticationProviderTobikoCloud.name,
39+
authProvider,
40+
{ supportsMultipleAccounts: false }
41+
)
42+
)
43+
traceInfo("Authentication provider registered")
44+
45+
context.subscriptions.push(
46+
vscode.commands.registerCommand("sqlmesh.signin", signIn(authProvider))
47+
)
48+
49+
context.subscriptions.push(
50+
vscode.commands.registerCommand("sqlmesh.signout", signOut(authProvider))
51+
)
52+
53+
context.subscriptions.push(
54+
vscode.commands.registerCommand("sqlmesh.format", async () => {
3555
const out = await actual_callout.format()
3656
if (out === 0) {
3757
vscode.window.showInformationMessage("Project formatted successfully")
3858
} else {
3959
vscode.window.showErrorMessage("Project format failed")
4060
}
41-
}
42-
))
61+
})
62+
)
4363

4464
lspClient = new LSPClient()
4565
await lspClient.start()
@@ -64,7 +84,7 @@ export async function activate(context: vscode.ExtensionContext) {
6484
})
6585
)
6686

67-
traceInfo("Extension activated")
87+
traceInfo("Extension activated")
6888
}
6989

7090
// This method is called when your extension is deactivated

0 commit comments

Comments
 (0)