Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit733fbbf

Browse files
authored
Do not require token auth with mTLS (#378)
1 parentf779aa2 commit733fbbf

File tree

5 files changed

+125
-72
lines changed

5 files changed

+125
-72
lines changed

‎package.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,17 @@
7474
"default":""
7575
},
7676
"coder.tlsCertFile": {
77-
"markdownDescription":"Path to file for TLS client cert",
77+
"markdownDescription":"Path to file for TLS client cert. When specified, token authorization will be skipped.",
7878
"type":"string",
7979
"default":""
8080
},
8181
"coder.tlsKeyFile": {
82-
"markdownDescription":"Path to file for TLS client key",
82+
"markdownDescription":"Path to file for TLS client key. When specified, token authorization will be skipped.",
8383
"type":"string",
8484
"default":""
8585
},
8686
"coder.tlsCaFile": {
87-
"markdownDescription":"Path to file for TLS certificate authority",
87+
"markdownDescription":"Path to file for TLS certificate authority.",
8888
"type":"string",
8989
"default":""
9090
},

‎src/api.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ import { getProxyForUrl } from "./proxy"
1010
import{Storage}from"./storage"
1111
import{expandPath}from"./util"
1212

13+
/**
14+
* Return whether the API will need a token for authorization.
15+
* If mTLS is in use (as specified by the cert or key files being set) then
16+
* token authorization is disabled. Otherwise, it is enabled.
17+
*/
18+
exportfunctionneedToken():boolean{
19+
constcfg=vscode.workspace.getConfiguration()
20+
constcertFile=expandPath(String(cfg.get("coder.tlsCertFile")??"").trim())
21+
constkeyFile=expandPath(String(cfg.get("coder.tlsKeyFile")??"").trim())
22+
return!certFile&&!keyFile
23+
}
24+
25+
/**
26+
* Create a new agent based off the current settings.
27+
*/
1328
asyncfunctioncreateHttpAgent():Promise<ProxyAgent>{
1429
constcfg=vscode.workspace.getConfiguration()
1530
constinsecure=Boolean(cfg.get("coder.insecure"))
@@ -32,7 +47,16 @@ async function createHttpAgent(): Promise<ProxyAgent> {
3247
})
3348
}
3449

50+
// The agent is a singleton so we only have to listen to the configuration once
51+
// (otherwise we would have to carefully dispose agents to remove their
52+
// configuration listeners), and to share the connection pool.
3553
letagent:Promise<ProxyAgent>|undefined=undefined
54+
55+
/**
56+
* Get the existing agent or create one if necessary. On settings change,
57+
* recreate the agent. The agent on the client is not automatically updated;
58+
* this must be called before every request to get the latest agent.
59+
*/
3660
asyncfunctiongetHttpAgent():Promise<ProxyAgent>{
3761
if(!agent){
3862
vscode.workspace.onDidChangeConfiguration((e)=>{

‎src/commands.ts‎

Lines changed: 85 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Api } from "coder/site/src/api/api"
22
import{getErrorMessage}from"coder/site/src/api/errors"
33
import{User,Workspace,WorkspaceAgent}from"coder/site/src/api/typesGenerated"
44
import*asvscodefrom"vscode"
5-
import{makeCoderSdk}from"./api"
5+
import{makeCoderSdk,needToken}from"./api"
66
import{extractAgents}from"./api-helper"
77
import{CertificateError}from"./error"
88
import{Storage}from"./storage"
@@ -137,88 +137,48 @@ export class Commands {
137137
* ask for it first with a menu showing recent URLs and CODER_URL, if set.
138138
*/
139139
publicasynclogin(...args:string[]):Promise<void>{
140-
consturl=awaitthis.maybeAskUrl(args[0])
140+
// Destructure would be nice but VS Code can pass undefined which errors.
141+
constinputUrl=args[0]
142+
constinputToken=args[1]
143+
constinputLabel=args[2]
144+
145+
consturl=awaitthis.maybeAskUrl(inputUrl)
141146
if(!url){
142147
return
143148
}
144149

145150
// It is possible that we are trying to log into an old-style host, in which
146151
// case we want to write with the provided blank label instead of generating
147152
// a host label.
148-
constlabel=typeofargs[2]==="undefined" ?toSafeHost(url) :args[2]
149-
150-
// Use a temporary client to avoid messing with the global one while trying
151-
// to log in.
152-
constrestClient=awaitmakeCoderSdk(url,undefined,this.storage)
153-
154-
letuser:User|undefined
155-
lettoken:string|undefined=args[1]
156-
if(!token){
157-
constopened=awaitvscode.env.openExternal(vscode.Uri.parse(`${url}/cli-auth`))
158-
if(!opened){
159-
vscode.window.showWarningMessage("You must accept the URL prompt to generate an API key.")
160-
return
161-
}
162-
163-
token=awaitvscode.window.showInputBox({
164-
title:"Coder API Key",
165-
password:true,
166-
placeHolder:"Copy your API key from the opened browser page.",
167-
value:awaitthis.storage.getSessionToken(),
168-
ignoreFocusOut:true,
169-
validateInput:async(value)=>{
170-
restClient.setSessionToken(value)
171-
try{
172-
user=awaitrestClient.getAuthenticatedUser()
173-
if(!user){
174-
thrownewError("Failed to get authenticated user")
175-
}
176-
}catch(err){
177-
// For certificate errors show both a notification and add to the
178-
// text under the input box, since users sometimes miss the
179-
// notification.
180-
if(errinstanceofCertificateError){
181-
err.showNotification()
153+
constlabel=typeofinputLabel==="undefined" ?toSafeHost(url) :inputLabel
182154

183-
return{
184-
message:err.x509Err||err.message,
185-
severity:vscode.InputBoxValidationSeverity.Error,
186-
}
187-
}
188-
// This could be something like the header command erroring or an
189-
// invalid session token.
190-
constmessage=getErrorMessage(err,"no response from the server")
191-
return{
192-
message:"Failed to authenticate: "+message,
193-
severity:vscode.InputBoxValidationSeverity.Error,
194-
}
195-
}
196-
},
197-
})
198-
}
199-
if(!token||!user){
200-
return
155+
// Try to get a token from the user, if we need one, and their user.
156+
constres=awaitthis.maybeAskToken(url,inputToken)
157+
if(!res){
158+
return// The user aborted.
201159
}
202160

203-
// The URL and token are good; authenticate the global client.
161+
// The URL is good and the token is either good or not required; authorize
162+
// the global client.
204163
this.restClient.setHost(url)
205-
this.restClient.setSessionToken(token)
164+
this.restClient.setSessionToken(res.token)
206165

207166
// Store these to be used in later sessions.
208167
awaitthis.storage.setUrl(url)
209-
awaitthis.storage.setSessionToken(token)
168+
awaitthis.storage.setSessionToken(res.token)
210169

211170
// Store on disk to be used by the cli.
212-
awaitthis.storage.configureCli(label,url,token)
171+
awaitthis.storage.configureCli(label,url,res.token)
213172

173+
// These contexts control various menu items and the sidebar.
214174
awaitvscode.commands.executeCommand("setContext","coder.authenticated",true)
215-
if(user.roles.find((role)=>role.name==="owner")){
175+
if(res.user.roles.find((role)=>role.name==="owner")){
216176
awaitvscode.commands.executeCommand("setContext","coder.isOwner",true)
217177
}
218178

219179
vscode.window
220180
.showInformationMessage(
221-
`Welcome to Coder,${user.username}!`,
181+
`Welcome to Coder,${res.user.username}!`,
222182
{
223183
detail:"You can now use the Coder extension to manage your Coder instance.",
224184
},
@@ -234,6 +194,71 @@ export class Commands {
234194
vscode.commands.executeCommand("coder.refreshWorkspaces")
235195
}
236196

197+
/**
198+
* If necessary, ask for a token, and keep asking until the token has been
199+
* validated. Return the token and user that was fetched to validate the
200+
* token.
201+
*/
202+
privateasyncmaybeAskToken(url:string,token:string):Promise<{user:User;token:string}|null>{
203+
constrestClient=awaitmakeCoderSdk(url,token,this.storage)
204+
if(!needToken()){
205+
return{
206+
// For non-token auth, we write a blank token since the `vscodessh`
207+
// command currently always requires a token file.
208+
token:"",
209+
user:awaitrestClient.getAuthenticatedUser(),
210+
}
211+
}
212+
213+
// This prompt is for convenience; do not error if they close it since
214+
// they may already have a token or already have the page opened.
215+
awaitvscode.env.openExternal(vscode.Uri.parse(`${url}/cli-auth`))
216+
217+
// For token auth, start with the existing token in the prompt or the last
218+
// used token. Once submitted, if there is a failure we will keep asking
219+
// the user for a new token until they quit.
220+
letuser:User|undefined
221+
constvalidatedToken=awaitvscode.window.showInputBox({
222+
title:"Coder API Key",
223+
password:true,
224+
placeHolder:"Paste your API key.",
225+
value:token||(awaitthis.storage.getSessionToken()),
226+
ignoreFocusOut:true,
227+
validateInput:async(value)=>{
228+
restClient.setSessionToken(value)
229+
try{
230+
user=awaitrestClient.getAuthenticatedUser()
231+
}catch(err){
232+
// For certificate errors show both a notification and add to the
233+
// text under the input box, since users sometimes miss the
234+
// notification.
235+
if(errinstanceofCertificateError){
236+
err.showNotification()
237+
238+
return{
239+
message:err.x509Err||err.message,
240+
severity:vscode.InputBoxValidationSeverity.Error,
241+
}
242+
}
243+
// This could be something like the header command erroring or an
244+
// invalid session token.
245+
constmessage=getErrorMessage(err,"no response from the server")
246+
return{
247+
message:"Failed to authenticate: "+message,
248+
severity:vscode.InputBoxValidationSeverity.Error,
249+
}
250+
}
251+
},
252+
})
253+
254+
if(validatedToken&&user){
255+
return{token:validatedToken, user}
256+
}
257+
258+
// User aborted.
259+
returnnull
260+
}
261+
237262
/**
238263
* View the logs for the currently connected workspace.
239264
*/

‎src/extension.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import axios, { isAxiosError } from "axios"
33
import{getErrorMessage}from"coder/site/src/api/errors"
44
import*asmodulefrom"module"
55
import*asvscodefrom"vscode"
6-
import{makeCoderSdk}from"./api"
6+
import{makeCoderSdk,needToken}from"./api"
77
import{errToStr}from"./api-helper"
88
import{Commands}from"./commands"
99
import{CertificateError,getErrorDetail}from"./error"
@@ -96,8 +96,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
9696
}
9797

9898
// If the token is missing we will get a 401 later and the user will be
99-
// prompted to sign in again, so we do not need to ensure it is set.
100-
consttoken=params.get("token")
99+
// prompted to sign in again, so we do not need to ensure it is set now.
100+
// For non-token auth, we write a blank token since the `vscodessh`
101+
// command currently always requires a token file. However, if there is
102+
// a query parameter for non-token auth go ahead and use it anyway; all
103+
// that really matters is the file is created.
104+
consttoken=needToken() ?params.get("token") :(params.get("token")??"")
101105
if(token){
102106
restClient.setSessionToken(token)
103107
awaitstorage.setSessionToken(token)

‎src/storage.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -435,10 +435,10 @@ export class Storage {
435435
/**
436436
* Configure the CLI for the deployment with the provided label.
437437
*
438-
* Falseyvaluesare a no-op; we avoid unconfiguring the CLI to avoid breaking
439-
* existing connections.
438+
* FalseyURLs and null tokensare a no-op; we avoid unconfiguring the CLI to
439+
*avoid breakingexisting connections.
440440
*/
441-
publicasyncconfigureCli(label:string,url:string|undefined,token:string|undefined|null){
441+
publicasyncconfigureCli(label:string,url:string|undefined,token:string|null){
442442
awaitPromise.all([this.updateUrlForCli(label,url),this.updateTokenForCli(label,token)])
443443
}
444444

@@ -459,15 +459,15 @@ export class Storage {
459459
/**
460460
* Update the session token for a deployment with the provided label on disk
461461
* which can be used by the CLI via --session-token-file. If the token is
462-
*falsey, do nothing.
462+
*null, do nothing.
463463
*
464464
* If the label is empty, read the old deployment-unaware config instead.
465465
*/
466466
privateasyncupdateTokenForCli(label:string,token:string|undefined|null){
467-
if(token){
467+
if(token!==null){
468468
consttokenPath=this.getSessionTokenPath(label)
469469
awaitfs.mkdir(path.dirname(tokenPath),{recursive:true})
470-
awaitfs.writeFile(tokenPath,token)
470+
awaitfs.writeFile(tokenPath,token??"")
471471
}
472472
}
473473

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp