Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 87 additions & 13 deletions src/git/askpass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<void> {
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<string> {
Expand Down
3 changes: 3 additions & 0 deletions src/git/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions src/git/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,10 @@ export class Model implements IRepositoryResolver, IRemoteSourcePublisherRegistr
return this.akspass.registerCredentialsProvider(provider);
}

async clearCredentials(host: string): Promise<void> {
await this.akspass.clearCredentials(host);
}

registerPushErrorHandler(handler: PushErrorHandler): IDisposable {
this.pushErrorHandlers.add(handler);
return Disposable.toDisposable(() => this.pushErrorHandlers.delete(handler));
Expand Down
8 changes: 4 additions & 4 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ async function findShell(logger?: LogOutputChannel): Promise<string | undefined>
return result;
}

async function createModel(logger: LogOutputChannel, disposables: IDisposable[]): Promise<Model> {
async function createModel(logger: LogOutputChannel, disposables: IDisposable[], ctx?: Acode.PluginContext | null): Promise<Model> {
const shell = await findShell(logger);
const info = await findGit();

Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down