diff --git a/src/git/askpass.ts b/src/git/askpass.ts index 7b42b28..a8d280b 100644 --- a/src/git/askpass.ts +++ b/src/git/askpass.ts @@ -140,7 +140,8 @@ export class AskPass implements IIPCHandler { constructor( private rootPath: string, ipc: IIPCServer | undefined, - private logger: LogOutputChannel + private logger: LogOutputChannel, + private ctx?: Acode.PluginContext | null ) { if (ipc) { this.disposable = ipc.registerHandler('askpass', this); @@ -213,34 +214,107 @@ export class AskPass implements IIPCHandler { const uri = new URL(host); const authority = uri.host; const password = /password/i.test(request); - const cached = this.cache.get(authority); + const isSecureStoreAvailable = !!(this.ctx && typeof this.ctx.getSecret === 'function' && typeof this.ctx.setSecret === 'function'); - if (cached && password) { - this.cache.delete(authority); - return cached.password; - } + if (password) { + let cached = this.cache.get(authority); + + if (cached && cached.password) { + this.cache.delete(authority); + return cached.password; + } + + if (isSecureStoreAvailable) { + try { + const storedUsername = await this.ctx!.getSecret(`${authority}:username`, ''); + const storedPassword = await this.ctx!.getSecret(`${authority}:password`, ''); + if (storedUsername && storedPassword) { + this.cache.delete(authority); + return storedPassword; + } + } catch (err) { + this.logger.error(`[Askpass][handleAskpass] error fetching password from secure store: ${err}`); + } + } + + const options = { placeholder: request, required: true }; + const enteredPassword = await prompt(`Git: ${host}`, '', 'text', options); + + if (enteredPassword) { + const usernameVal = cached?.username || uri.username || ''; + if (usernameVal && isSecureStoreAvailable) { + try { + await this.ctx!.setSecret(`${authority}:username`, usernameVal); + await this.ctx!.setSecret(`${authority}:password`, enteredPassword); + } catch (err) { + this.logger.error(`[Askpass][handleAskpass] error saving password to secure store: ${err}`); + } + } + return enteredPassword; + } + + return ''; + } else { + let cached = this.cache.get(authority); + + if (!cached && isSecureStoreAvailable) { + try { + const storedUsername = await this.ctx!.getSecret(`${authority}:username`, ''); + const storedPassword = await this.ctx!.getSecret(`${authority}:password`, ''); + if (storedUsername && storedPassword) { + cached = { username: storedUsername, password: storedPassword }; + this.cache.set(authority, cached); + setTimeout(() => this.cache.delete(authority), 60_000); + } + } catch (err) { + this.logger.error(`[Askpass][handleAskpass] error fetching username from secure store: ${err}`); + } + } + + if (cached) { + return cached.username; + } - if (!password) { for (const credentialsProvider of this.credentialsProviders) { try { const credentials = await credentialsProvider.getCredentials(host); if (credentials) { this.cache.set(authority, credentials); setTimeout(() => this.cache.delete(authority), 60_000); + if (isSecureStoreAvailable) { + await this.ctx!.setSecret(`${authority}:username`, credentials.username); + await this.ctx!.setSecret(`${authority}:password`, credentials.password); + } return credentials.username; } } catch { } } - } - const options = { placeholder: request, required: true }; - const result = await prompt(`Git: ${host}`, '', 'text', options); + const options = { placeholder: request, required: true }; + const enteredUsername = await prompt(`Git: ${host}`, '', 'text', options); + + if (enteredUsername) { + this.cache.set(authority, { username: enteredUsername, password: '' }); + return enteredUsername; + } - if (result) { - return result; + return ''; } + } - return ''; + async clearCredentials(host: string): Promise { + try { + const uri = new URL(host); + const authority = uri.host; + this.cache.delete(authority); + const isSecureStoreAvailable = !!(this.ctx && typeof this.ctx.getSecret === 'function' && typeof this.ctx.setSecret === 'function'); + if (isSecureStoreAvailable) { + await this.ctx!.setSecret(`${authority}:username`, ''); + await this.ctx!.setSecret(`${authority}:password`, ''); + } + } catch (err) { + this.logger.error(`[Askpass][clearCredentials] error: ${err}`); + } } async handleSSHAskpass(argv: string[]): Promise { diff --git a/src/git/commands.ts b/src/git/commands.ts index 3eb390a..0795888 100644 --- a/src/git/commands.ts +++ b/src/git/commands.ts @@ -3080,6 +3080,9 @@ export class CommandCenter { message = match ? `Failed to authenticate to git remote: ${match[1]}` : 'Failed to authenticate to git remote.'; + if (match && match[1]) { + this.model.clearCredentials(match[1]); + } break; } case GitErrorCodes.NoUserNameConfigured: diff --git a/src/git/model.ts b/src/git/model.ts index f22da83..589d971 100644 --- a/src/git/model.ts +++ b/src/git/model.ts @@ -815,6 +815,10 @@ export class Model implements IRepositoryResolver, IRemoteSourcePublisherRegistr return this.akspass.registerCredentialsProvider(provider); } + async clearCredentials(host: string): Promise { + await this.akspass.clearCredentials(host); + } + registerPushErrorHandler(handler: PushErrorHandler): IDisposable { this.pushErrorHandlers.add(handler); return Disposable.toDisposable(() => this.pushErrorHandlers.delete(handler)); diff --git a/src/main.ts b/src/main.ts index c030b1a..5f6cac0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -118,7 +118,7 @@ async function findShell(logger?: LogOutputChannel): Promise return result; } -async function createModel(logger: LogOutputChannel, disposables: IDisposable[]): Promise { +async function createModel(logger: LogOutputChannel, disposables: IDisposable[], ctx?: Acode.PluginContext | null): Promise { const shell = await findShell(logger); const info = await findGit(); @@ -134,7 +134,7 @@ async function createModel(logger: LogOutputChannel, disposables: IDisposable[]) logger.error(`[main] Failed to create git IPC: ${err}`); } - const askpass = new AskPass(rootPath, ipcServer, logger); + const askpass = new AskPass(rootPath, ipcServer, logger, ctx); const gitEditor = new GitEditor(rootPath, ipcServer, logger); await askpass.setupScripts(); await gitEditor.setupScript(); @@ -278,12 +278,12 @@ async function initialize(baseUrl: string, options: Acode.PluginInitOptions): Pr const onConfigChange = Event.filter(config.onDidChangeConfiguration, e => e.affectsConfiguration('vcgit')); const onEnabled = Event.filter(onConfigChange, () => config.get('vcgit')?.enabled === true); const result = new GitPluginImpl(); - Event.toPromise(onEnabled).then(async () => result.model = await createModel(logger, disposables)); + Event.toPromise(onEnabled).then(async () => result.model = await createModel(logger, disposables, options.ctx)); return result; } try { - const model = await createModel(logger, disposables); + const model = await createModel(logger, disposables, options.ctx); return new GitPluginImpl(model); } catch (err: any) { console.warn(err.message);