Skip to content

Commit e6ad1e2

Browse files
authored
fix(codecatalyst): avoid PAT in git clone URL
Problem: The private token (PAT) is part of the git clone URL in .git/config Solution: Use vscode-git extension CredentialProvider API, which stores the token in the OS keychain. Remove clone support on Cloud9.
1 parent 4ba9ec6 commit e6ad1e2

File tree

9 files changed

+84
-84
lines changed

9 files changed

+84
-84
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,7 +1281,7 @@
12811281
},
12821282
{
12831283
"command": "aws.codecatalyst.cloneRepo",
1284-
"when": "view == aws.codecatalyst",
1284+
"when": "view == aws.codecatalyst && !isCloud9",
12851285
"group": "1_codeCatalyst@1"
12861286
},
12871287
{
@@ -1998,7 +1998,8 @@
19981998
{
19991999
"command": "aws.codecatalyst.cloneRepo",
20002000
"title": "%AWS.command.codecatalyst.cloneRepo%",
2001-
"category": "AWS"
2001+
"category": "AWS",
2002+
"enablement": "!isCloud9"
20022003
},
20032004
{
20042005
"command": "aws.codecatalyst.createDevEnv",

src/codecatalyst/activation.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { dontShow } from '../shared/localizedText'
2020
import { isCloud9 } from '../shared/extensionUtilities'
2121
import { watchBetaVSIX } from './beta'
2222
import { Commands } from '../shared/vscode/commands2'
23+
import { getCodeCatalystConfig } from '../shared/clients/codecatalystClient'
2324

2425
const localize = nls.loadMessageBundle()
2526

@@ -36,11 +37,21 @@ export async function activate(ctx: ExtContext): Promise<void> {
3637
...Object.values(CodeCatalystCommands.declared).map(c => c.register(commands))
3738
)
3839

39-
GitExtension.instance.registerRemoteSourceProvider(remoteSourceProvider).then(disposable => {
40-
ctx.extensionContext.subscriptions.push(disposable)
41-
})
42-
4340
if (!isCloud9()) {
41+
GitExtension.instance.registerRemoteSourceProvider(remoteSourceProvider).then(disposable => {
42+
ctx.extensionContext.subscriptions.push(disposable)
43+
})
44+
45+
GitExtension.instance
46+
.registerCredentialsProvider({
47+
getCredentials(uri: vscode.Uri) {
48+
if (uri.authority.endsWith(getCodeCatalystConfig().gitHostname)) {
49+
return commands.withClient(client => authProvider.getCredentialsForGit(client))
50+
}
51+
},
52+
})
53+
.then(disposable => ctx.extensionContext.subscriptions.push(disposable))
54+
4455
watchRestartingDevEnvs(ctx, authProvider)
4556
watchBetaVSIX()
4657
}

src/codecatalyst/auth.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,20 @@ import { ConnectedCodeCatalystClient } from '../shared/clients/codecatalystClien
88
import { isCloud9 } from '../shared/extensionUtilities'
99
import { Auth, isBuilderIdConnection, Connection, SsoConnection, codecatalystScopes } from '../credentials/auth'
1010
import { getSecondaryAuth } from '../credentials/secondaryAuth'
11+
import { getLogger } from '../shared/logger'
1112

1213
// Secrets stored on the macOS keychain appear as individual entries for each key
1314
// This is fine so long as the user has only a few accounts. Otherwise this should
1415
// store secrets as a map.
1516
export class CodeCatalystAuthStorage {
1617
public constructor(private readonly secrets: vscode.SecretStorage) {}
1718

18-
public async getPat(id: string): Promise<string | undefined> {
19-
return this.secrets.get(`codecatalyst.pat.${id}`)
19+
public async getPat(username: string): Promise<string | undefined> {
20+
return this.secrets.get(`codecatalyst.pat.${username}`)
2021
}
2122

22-
public async storePat(id: string, pat: string): Promise<void> {
23-
await this.secrets.store(`codecatalyst.pat.${id}`, pat)
23+
public async storePat(username: string, pat: string): Promise<void> {
24+
await this.secrets.store(`codecatalyst.pat.${username}`, pat)
2425
}
2526
}
2627

@@ -46,19 +47,34 @@ export class CodeCatalystAuthenticationProvider {
4647
}
4748

4849
// Get rid of this? Not sure where to put PAT code.
49-
public async getPat(client: ConnectedCodeCatalystClient): Promise<string> {
50-
const stored = await this.storage.getPat(client.identity.id)
50+
public async getPat(client: ConnectedCodeCatalystClient, username = client.identity.name): Promise<string> {
51+
const stored = await this.storage.getPat(username)
5152

5253
if (stored) {
5354
return stored
5455
}
5556

5657
const resp = await client.createAccessToken({ name: 'aws-toolkits-vscode-token' })
57-
await this.storage.storePat(client.identity.id, resp.secret)
58+
await this.storage.storePat(username, resp.secret)
5859

5960
return resp.secret
6061
}
6162

63+
public async getCredentialsForGit(client: ConnectedCodeCatalystClient) {
64+
getLogger().verbose(`codecatalyst (git): attempting to provide credentials`)
65+
66+
const username = client.identity.name
67+
68+
try {
69+
return {
70+
username,
71+
password: await this.getPat(client, username),
72+
}
73+
} catch (err) {
74+
getLogger().verbose(`codecatalyst (git): failed to get credentials for user "${username}": %s`, err)
75+
}
76+
}
77+
6278
public async removeSavedConnection() {
6379
await this.secondaryAuth.removeConnection()
6480
}

src/codecatalyst/commands.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
ConnectedCodeCatalystClient,
1919
CodeCatalystResource,
2020
} from '../shared/clients/codecatalystClient'
21-
import { createClientFactory, DevEnvironmentId, getConnectedDevEnv, getRepoCloneUrl, openDevEnv } from './model'
21+
import { createClientFactory, DevEnvironmentId, getConnectedDevEnv, openDevEnv } from './model'
2222
import { showConfigureDevEnv } from './vue/configure/backend'
2323
import { showCreateDevEnv } from './vue/create/backend'
2424
import { CancellationError } from '../shared/utilities/timeoutUtils'
@@ -35,12 +35,6 @@ export async function listCommands(): Promise<void> {
3535

3636
/** "Clone CodeCatalyst Repository" command. */
3737
export async function cloneCodeCatalystRepo(client: ConnectedCodeCatalystClient, url?: vscode.Uri): Promise<void> {
38-
async function getPat() {
39-
// FIXME: make it easier to go from auth -> client so we don't need to do this
40-
const auth = CodeCatalystAuthenticationProvider.fromContext(globals.context)
41-
return auth.getPat(client)
42-
}
43-
4438
let resource: { name: string; project: string; org: string }
4539
if (!url) {
4640
const r = await selectCodeCatalystResource(client, 'repo')
@@ -56,16 +50,11 @@ export async function cloneCodeCatalystRepo(client: ConnectedCodeCatalystClient,
5650
resource = { name: repo, project, org }
5751
}
5852

59-
const uri = await getRepoCloneUrl(
60-
client,
61-
{
62-
spaceName: resource.org,
63-
projectName: resource.project,
64-
sourceRepositoryName: resource.name,
65-
},
66-
client.identity.name,
67-
await getPat()
68-
)
53+
const uri = await client.getRepoCloneUrl({
54+
spaceName: resource.org,
55+
projectName: resource.project,
56+
sourceRepositoryName: resource.name,
57+
})
6958
await vscode.commands.executeCommand('git.clone', uri)
7059
}
7160

src/codecatalyst/explorer.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as vscode from 'vscode'
77
import { RootNode } from '../awsexplorer/localExplorer'
8-
import { createBuilderIdConnection, isBuilderIdConnection } from '../credentials/auth'
8+
import { Connection, createBuilderIdConnection, isBuilderIdConnection } from '../credentials/auth'
99
import { DevEnvironment } from '../shared/clients/codecatalystClient'
1010
import { UnknownError } from '../shared/errors'
1111
import { isCloud9 } from '../shared/extensionUtilities'
@@ -30,6 +30,14 @@ const learnMoreCommand = Commands.register('aws.learnMore', async (docsUrl: vsco
3030
return vscode.env.openExternal(docsUrl)
3131
})
3232

33+
// Only used in rare cases on C9
34+
const reauth = Commands.register(
35+
'_aws.codecatalyst.reauthenticate',
36+
async (conn: Connection, authProvider: CodeCatalystAuthenticationProvider) => {
37+
await authProvider.auth.reauthenticate(conn)
38+
}
39+
)
40+
3341
function getLocalCommands(auth: CodeCatalystAuthenticationProvider) {
3442
const docsUrl = isCloud9() ? codecatalyst.docs.cloud9.overview : codecatalyst.docs.vscode.overview
3543
if (!isBuilderIdConnection(auth.activeConnection)) {
@@ -45,19 +53,20 @@ function getLocalCommands(auth: CodeCatalystAuthenticationProvider) {
4553
]
4654
}
4755

48-
const cmds = [
49-
CodeCatalystCommands.declared.cloneRepo.build().asTreeNode({
50-
label: 'Clone Repository',
51-
iconPath: getIcon('vscode-symbol-namespace'),
52-
}),
53-
]
54-
5556
if (isCloud9()) {
56-
return cmds
57+
const item = reauth.build(auth.activeConnection, auth).asTreeNode({
58+
label: 'Failed to get the current Dev Environment. Click to try again.',
59+
iconPath: getIcon(`vscode-error`),
60+
})
61+
62+
return [item]
5763
}
5864

5965
return [
60-
...cmds,
66+
CodeCatalystCommands.declared.cloneRepo.build().asTreeNode({
67+
label: 'Clone Repository',
68+
iconPath: getIcon('vscode-symbol-namespace'),
69+
}),
6170
CodeCatalystCommands.declared.openDevEnv.build().asTreeNode({
6271
label: 'Open Dev Environment',
6372
iconPath: getIcon('vscode-vm-connect'),

src/codecatalyst/model.ts

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import { ensureDependencies, HOST_NAME_PREFIX } from './tools'
2727
import { isCodeCatalystVSCode } from './utils'
2828
import { Timeout } from '../shared/utilities/timeoutUtils'
2929
import { Commands } from '../shared/vscode/commands2'
30-
import * as codecatalyst from '../../types/clientcodecatalyst'
3130

3231
export type DevEnvironmentId = Pick<DevEnvironment, 'id' | 'org' | 'project'>
3332

@@ -300,36 +299,6 @@ export async function getDevfileLocation(client: DevEnvClient, root?: vscode.Uri
300299
return vscode.Uri.joinPath(rootDirectory, devfileLocation)
301300
}
302301

303-
function toCodeCatalystGitUri(username: string, token: string, cloneUrl: string): string {
304-
// "https://[email protected].…" => "git.gamma.…"
305-
let url = cloneUrl.replace(/https:\/\/[^@\/]+@/, '')
306-
if (url === cloneUrl) {
307-
// URL didn't change, so it's missing the "user@" part.
308-
// "https://git.gamma.…" => "git.gamma.…"
309-
url = cloneUrl.replace('https://', '')
310-
}
311-
return `https://${username}:${token}@${url}`
312-
}
313-
314-
/**
315-
* Gets a URL including username and password (PAT) that can be used by git to clone the given CodeCatalyst repo.
316-
*
317-
* Example: "https://user:[email protected].…"
318-
*
319-
* @param args
320-
* @returns Clone URL (example: "https://user:[email protected].…")
321-
*/
322-
export async function getRepoCloneUrl(
323-
client: ConnectedCodeCatalystClient,
324-
args: codecatalyst.GetSourceRepositoryCloneUrlsRequest,
325-
user: string,
326-
password: string
327-
): Promise<string> {
328-
const url = await client.getRepoCloneUrl(args)
329-
const cloneurl = toCodeCatalystGitUri(user, password, url)
330-
return cloneurl
331-
}
332-
333302
/**
334303
* Given a collection of CodeCatalyst repos, try to find a corresponding devenv, if any
335304
*/

src/codecatalyst/repos/remoteSourceProvider.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import * as vscode from 'vscode'
1010
import { RemoteSource, RemoteSourceProvider } from '../../../types/git'
1111
import { CodeCatalystAuthenticationProvider } from '../auth'
1212
import { createRepoLabel } from '../wizards/selectResource'
13-
import { getRepoCloneUrl } from '../model'
1413
import { CodeCatalystCommands } from '../commands'
1514
import { CodeCatalystRepo } from '../../shared/clients/codecatalystClient'
1615
import { getIcon, Icon } from '../../shared/icons'
@@ -57,17 +56,11 @@ export class CodeCatalystRemoteSourceProvider implements RemoteSourceProvider {
5756
return this.commands.withClient(async client => {
5857
const intoRemote = async (repo: CodeCatalystRepo): Promise<RemoteSource> => {
5958
const resource = { name: repo.name, project: repo.project.name, org: repo.org.name }
60-
const pat = await this.authProvider.getPat(client)
61-
const url = await getRepoCloneUrl(
62-
client,
63-
{
64-
spaceName: resource.org,
65-
projectName: resource.project,
66-
sourceRepositoryName: resource.name,
67-
},
68-
client.identity.name,
69-
pat
70-
)
59+
const url = await client.getRepoCloneUrl({
60+
spaceName: resource.org,
61+
projectName: resource.project,
62+
sourceRepositoryName: resource.name,
63+
})
7164

7265
return {
7366
name: createRepoLabel(repo),

src/shared/clients/codecatalystClient.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,9 @@ class CodeCatalystClientInternal {
516516
*/
517517
public async getRepoCloneUrl(args: codecatalyst.GetSourceRepositoryCloneUrlsRequest): Promise<string> {
518518
const r = await this.call(this.sdkClient.getSourceRepositoryCloneUrls(args), false)
519-
return r.https
519+
520+
// The git extension skips over credential providers if the username is included in the authority
521+
return `https://${r.https.replace(/.*@/, '')}`
520522
}
521523

522524
public async createDevEnvironment(args: codecatalyst.CreateDevEnvironmentRequest): Promise<DevEnvironment> {

src/shared/extensions/git.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,4 +390,14 @@ export class GitExtension {
390390

391391
return api.registerRemoteSourceProvider(provider)
392392
}
393+
394+
public async registerCredentialsProvider(provider: GitTypes.CredentialsProvider): Promise<vscode.Disposable> {
395+
const api = await this.validateApi('git: extension disabled, unable to register credentials provider')
396+
397+
if (!api) {
398+
return { dispose: () => {} }
399+
}
400+
401+
return api.registerCredentialsProvider(provider)
402+
}
393403
}

0 commit comments

Comments
 (0)