|
1 | | -Use our own GitHub auth relay server |
| 1 | +Add the ability to provide a GitHub token |
2 | 2 |
|
3 | | -Microsoft's does not work with self-hosted instances so we run our own. |
| 3 | +To test install the GitHub PR extension and start code-server with GITHUB_TOKEN |
| 4 | +or set github-auth in the config file. The extension should be authenticated. |
4 | 5 |
|
5 | | -Also add an extra set of scopes so that tokens provided via --github-auth will |
6 | | -work for the PR extension. |
7 | | - |
8 | | -Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts |
9 | | -=================================================================== |
10 | | ---- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts |
11 | | -+++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts |
12 | | -@@ -277,7 +277,7 @@ export class WebClientServer { |
13 | | - id: generateUuid(), |
14 | | - providerId: 'github', |
15 | | - accessToken: this._environmentService.args['github-auth'], |
16 | | -- scopes: [['user:email'], ['repo']] |
17 | | -+ scopes: [['read:user', 'user:email', 'repo'], ['user:email'], ['repo']] |
18 | | - } : undefined; |
19 | | - const base = relativeRoot(getOriginalUrl(req)) |
20 | | - const vscodeBase = relativePath(getOriginalUrl(req)) |
21 | | -Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts |
| 6 | +Index: code-server/lib/vscode/src/vs/platform/credentials/node/credentialsMainService.ts |
22 | 7 | =================================================================== |
23 | | ---- code-server.orig/lib/vscode/src/vs/code/browser/workbench/workbench.ts |
24 | | -+++ code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts |
25 | | -@@ -17,6 +17,7 @@ import { isFolderToOpen, isWorkspaceToOp |
26 | | - import { create, ICredentialsProvider, IURLCallbackProvider, IWorkbenchConstructionOptions, IWorkspace, IWorkspaceProvider } from 'vs/workbench/workbench.web.main'; |
27 | | - import { posix } from 'vs/base/common/path'; |
28 | | - import { ltrim } from 'vs/base/common/strings'; |
29 | | -+import { equals as arrayEquals } from 'vs/base/common/arrays'; |
30 | | - |
31 | | - interface ICredential { |
32 | | - service: string; |
33 | | -@@ -24,6 +25,13 @@ interface ICredential { |
34 | | - password: string; |
35 | | - } |
| 8 | +--- code-server.orig/lib/vscode/src/vs/platform/credentials/node/credentialsMainService.ts |
| 9 | ++++ code-server/lib/vscode/src/vs/platform/credentials/node/credentialsMainService.ts |
| 10 | +@@ -5,18 +5,32 @@ |
36 | 11 |
|
| 12 | + import { InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials'; |
| 13 | + import { ILogService } from 'vs/platform/log/common/log'; |
| 14 | +-import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; |
| 15 | ++import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; |
| 16 | + import { IProductService } from 'vs/platform/product/common/productService'; |
| 17 | + import { BaseCredentialsMainService, KeytarModule } from 'vs/platform/credentials/common/credentialsMainService'; |
| 18 | ++import { generateUuid } from 'vs/base/common/uuid'; |
| 19 | ++import { equals as arrayEquals } from 'vs/base/common/arrays'; |
| 20 | ++ |
37 | 21 | +interface IToken { |
38 | 22 | + accessToken: string |
39 | 23 | + account?: { label: string } |
40 | 24 | + id: string |
41 | 25 | + scopes: string[] |
42 | 26 | +} |
43 | | -+ |
44 | | - class LocalStorageCredentialsProvider implements ICredentialsProvider { |
45 | 27 |
|
46 | | - private static readonly CREDENTIALS_STORAGE_KEY = 'credentials.provider'; |
47 | | -@@ -51,6 +59,58 @@ class LocalStorageCredentialsProvider im |
48 | | - scopes, |
49 | | - accessToken: authSessionInfo!.accessToken |
50 | | - })))); |
| 28 | + export class CredentialsWebMainService extends BaseCredentialsMainService { |
| 29 | + |
| 30 | + constructor( |
| 31 | + @ILogService logService: ILogService, |
| 32 | +- @INativeEnvironmentService private readonly environmentMainService: INativeEnvironmentService, |
| 33 | ++ @IServerEnvironmentService private readonly environmentMainService: IServerEnvironmentService, |
| 34 | + @IProductService private readonly productService: IProductService, |
| 35 | + ) { |
| 36 | + super(logService); |
| 37 | ++ if (this.environmentMainService.args["github-auth"]) { |
| 38 | ++ this.storeGitHubToken(this.environmentMainService.args["github-auth"]).catch((error) => { |
| 39 | ++ this.logService.error('Failed to store provided GitHub token', error) |
| 40 | ++ }) |
| 41 | ++ } |
| 42 | + } |
| 43 | + |
| 44 | + // If the credentials service is running on the server, we add a suffix -server to differentiate from the location that the |
| 45 | +@@ -45,4 +59,62 @@ export class CredentialsWebMainService e |
| 46 | + } |
| 47 | + return this._keytarCache; |
| 48 | + } |
| 49 | ++ |
| 50 | ++ /** |
| 51 | ++ * Authenticate the GitHub authentication extension with CLI arguments. |
| 52 | ++ */ |
| 53 | ++ private async storeGitHubToken(githubToken: string): Promise<void> { |
| 54 | ++ const extensionId = 'vscode.github-authentication'; |
| 55 | ++ const service = `${await this.getSecretStoragePrefix()}${extensionId}`; |
| 56 | ++ const account = 'github.auth'; |
| 57 | ++ const scopes = [['read:user', 'user:email', 'repo']] |
| 58 | ++ |
| 59 | ++ // Oddly the scopes need to match exactly so we cannot just have one token |
| 60 | ++ // with all the scopes, instead we have to duplicate the token for each |
| 61 | ++ // expected set of scopes. |
| 62 | ++ const tokens: IToken[] = scopes.map((scopes) => ({ |
| 63 | ++ id: generateUuid(), |
| 64 | ++ scopes: scopes.sort(), // Sort for comparing later. |
| 65 | ++ accessToken: githubToken, |
| 66 | ++ })); |
51 | 67 | + |
52 | | -+ // Add tokens for extensions to use. This works for extensions like the |
53 | | -+ // pull requests one or GitLens. |
54 | | -+ const extensionId = `vscode.${authSessionInfo.providerId}-authentication`; |
55 | | -+ const service = `${product.urlProtocol}${extensionId}`; |
56 | | -+ const account = `${authSessionInfo.providerId}.auth`; |
57 | | -+ // Oddly the scopes need to match exactly so we cannot just have one token |
58 | | -+ // with all the scopes, instead we have to duplicate the token for each |
59 | | -+ // expected set of scopes. |
60 | | -+ const tokens: IToken[] = authSessionInfo.scopes.map((scopes) => ({ |
61 | | -+ id: authSessionInfo!.id, |
62 | | -+ scopes: scopes.sort(), // Sort for comparing later. |
63 | | -+ accessToken: authSessionInfo!.accessToken, |
64 | | -+ })); |
65 | | -+ this.getPassword(service, account).then((raw) => { |
66 | | -+ let existing: { |
67 | | -+ content: IToken[] |
68 | | -+ } | undefined; |
| 68 | ++ const raw = await this.getPassword(service, account) |
69 | 69 | + |
70 | | -+ if (raw) { |
71 | | -+ try { |
72 | | -+ const json = JSON.parse(raw); |
73 | | -+ json.content = JSON.parse(json.content); |
74 | | -+ existing = json; |
75 | | -+ } catch (error) { |
76 | | -+ console.log(error); |
77 | | -+ } |
78 | | -+ } |
| 70 | ++ let existing: { |
| 71 | ++ content: IToken[] |
| 72 | ++ } | undefined; |
79 | 73 | + |
80 | | -+ // Keep tokens for account and scope combinations we do not have in case |
81 | | -+ // there is an extension that uses scopes we have not accounted for (in |
82 | | -+ // these cases the user will need to manually authenticate the extension |
83 | | -+ // through the UI) or the user has tokens for other accounts. |
84 | | -+ if (existing?.content) { |
85 | | -+ existing.content = existing.content.filter((existingToken) => { |
86 | | -+ const scopes = existingToken.scopes.sort(); |
87 | | -+ return !(tokens.find((token) => { |
88 | | -+ return arrayEquals(scopes, token.scopes) |
89 | | -+ && token.account?.label === existingToken.account?.label; |
90 | | -+ })) |
91 | | -+ }) |
92 | | -+ } |
| 74 | ++ if (raw) { |
| 75 | ++ try { |
| 76 | ++ const json = JSON.parse(raw); |
| 77 | ++ json.content = JSON.parse(json.content); |
| 78 | ++ existing = json; |
| 79 | ++ } catch (error) { |
| 80 | ++ this.logService.error('Failed to parse existing GitHub credentials', error) |
| 81 | ++ } |
| 82 | ++ } |
93 | 83 | + |
94 | | -+ return this.setPassword(service, account, JSON.stringify({ |
95 | | -+ extensionId, |
96 | | -+ ...(existing || {}), |
97 | | -+ content: JSON.stringify([ |
98 | | -+ ...tokens, |
99 | | -+ ...(existing?.content || []), |
100 | | -+ ]) |
101 | | -+ })); |
| 84 | ++ // Keep tokens for account and scope combinations we do not have in case |
| 85 | ++ // there is an extension that uses scopes we have not accounted for (in |
| 86 | ++ // these cases the user will need to manually authenticate the extension |
| 87 | ++ // through the UI) or the user has tokens for other accounts. |
| 88 | ++ if (existing?.content) { |
| 89 | ++ existing.content = existing.content.filter((existingToken) => { |
| 90 | ++ const scopes = existingToken.scopes.sort(); |
| 91 | ++ return !(tokens.find((token) => { |
| 92 | ++ return arrayEquals(scopes, token.scopes) |
| 93 | ++ && token.account?.label === existingToken.account?.label; |
| 94 | ++ })) |
102 | 95 | + }) |
103 | | - } |
104 | | - } |
105 | | - |
| 96 | ++ } |
| 97 | ++ |
| 98 | ++ return this.setPassword(service, account, JSON.stringify({ |
| 99 | ++ extensionId, |
| 100 | ++ ...(existing || {}), |
| 101 | ++ content: JSON.stringify([ |
| 102 | ++ ...tokens, |
| 103 | ++ ...(existing?.content || []), |
| 104 | ++ ]) |
| 105 | ++ })); |
| 106 | ++ } |
| 107 | + } |
0 commit comments