Add the ability to provide a GitHub token To test install the GitHub PR extension and start code-server with GITHUB_TOKEN or set github-auth in the config file. The extension should be authenticated. Index: code-server/lib/vscode/src/vs/platform/credentials/node/credentialsMainService.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/platform/credentials/node/credentialsMainService.ts +++ code-server/lib/vscode/src/vs/platform/credentials/node/credentialsMainService.ts @@ -5,9 +5,18 @@ import { InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials'; import { ILogService } from 'vs/platform/log/common/log'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; import { IProductService } from 'vs/platform/product/common/productService'; import { BaseCredentialsMainService, KeytarModule } from 'vs/platform/credentials/common/credentialsMainService'; +import { generateUuid } from 'vs/base/common/uuid'; +import { equals as arrayEquals } from 'vs/base/common/arrays'; + +interface IToken { + accessToken: string + account?: { label: string } + id: string + scopes: string[] +} export class CredentialsWebMainService extends BaseCredentialsMainService { // Since we fallback to the in-memory credentials provider, we do not need to surface any Keytar load errors @@ -16,10 +25,15 @@ export class CredentialsWebMainService e constructor( @ILogService logService: ILogService, - @INativeEnvironmentService private readonly environmentMainService: INativeEnvironmentService, + @IServerEnvironmentService private readonly environmentMainService: IServerEnvironmentService, @IProductService private readonly productService: IProductService, ) { super(logService); + if (this.environmentMainService.args["github-auth"]) { + this.storeGitHubToken(this.environmentMainService.args["github-auth"]).catch((error) => { + this.logService.error('Failed to store provided GitHub token', error) + }) + } } // If the credentials service is running on the server, we add a suffix -server to differentiate from the location that the @@ -48,4 +62,59 @@ export class CredentialsWebMainService e } return this._keytarCache; } + + private async storeGitHubToken(githubToken: string): Promise { + const extensionId = 'vscode.github-authentication'; + const service = `${await this.getSecretStoragePrefix()}${extensionId}`; + const account = 'github.auth'; + const scopes = [['read:user', 'user:email', 'repo']] + + // Oddly the scopes need to match exactly so we cannot just have one token + // with all the scopes, instead we have to duplicate the token for each + // expected set of scopes. + const tokens: IToken[] = scopes.map((scopes) => ({ + id: generateUuid(), + scopes: scopes.sort(), // Sort for comparing later. + accessToken: githubToken, + })); + + const raw = await this.getPassword(service, account) + + let existing: { + content: IToken[] + } | undefined; + + if (raw) { + try { + const json = JSON.parse(raw); + json.content = JSON.parse(json.content); + existing = json; + } catch (error) { + this.logService.error('Failed to parse existing GitHub credentials', error) + } + } + + // Keep tokens for account and scope combinations we do not have in case + // there is an extension that uses scopes we have not accounted for (in + // these cases the user will need to manually authenticate the extension + // through the UI) or the user has tokens for other accounts. + if (existing?.content) { + existing.content = existing.content.filter((existingToken) => { + const scopes = existingToken.scopes.sort(); + return !(tokens.find((token) => { + return arrayEquals(scopes, token.scopes) + && token.account?.label === existingToken.account?.label; + })) + }) + } + + return this.setPassword(service, account, JSON.stringify({ + extensionId, + ...(existing || {}), + content: JSON.stringify([ + ...tokens, + ...(existing?.content || []), + ]) + })); + } }