|
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,59 @@ export class CredentialsWebMainService e |
| 46 | + } |
| 47 | + return this._keytarCache; |
| 48 | + } |
| 49 | ++ |
| 50 | ++ private async storeGitHubToken(githubToken: string): Promise<void> { |
| 51 | ++ const extensionId = 'vscode.github-authentication'; |
| 52 | ++ const service = `${await this.getSecretStoragePrefix()}${extensionId}`; |
| 53 | ++ const account = 'github.auth'; |
| 54 | ++ const scopes = [['read:user', 'user:email', 'repo']] |
| 55 | ++ |
| 56 | ++ // Oddly the scopes need to match exactly so we cannot just have one token |
| 57 | ++ // with all the scopes, instead we have to duplicate the token for each |
| 58 | ++ // expected set of scopes. |
| 59 | ++ const tokens: IToken[] = scopes.map((scopes) => ({ |
| 60 | ++ id: generateUuid(), |
| 61 | ++ scopes: scopes.sort(), // Sort for comparing later. |
| 62 | ++ accessToken: githubToken, |
| 63 | ++ })); |
51 | 64 | +
|
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; |
| 65 | ++ const raw = await this.getPassword(service, account) |
69 | 66 | +
|
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 |
| -+ } |
| 67 | ++ let existing: { |
| 68 | ++ content: IToken[] |
| 69 | ++ } | undefined; |
79 | 70 | +
|
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 |
| -+ } |
| 71 | ++ if (raw) { |
| 72 | ++ try { |
| 73 | ++ const json = JSON.parse(raw); |
| 74 | ++ json.content = JSON.parse(json.content); |
| 75 | ++ existing = json; |
| 76 | ++ } catch (error) { |
| 77 | ++ this.logService.error('Failed to parse existing GitHub credentials', error) |
| 78 | ++ } |
| 79 | ++ } |
93 | 80 | +
|
94 |
| -+ return this.setPassword(service, account, JSON.stringify({ |
95 |
| -+ extensionId, |
96 |
| -+ ...(existing || {}), |
97 |
| -+ content: JSON.stringify([ |
98 |
| -+ ...tokens, |
99 |
| -+ ...(existing?.content || []), |
100 |
| -+ ]) |
101 |
| -+ })); |
| 81 | ++ // Keep tokens for account and scope combinations we do not have in case |
| 82 | ++ // there is an extension that uses scopes we have not accounted for (in |
| 83 | ++ // these cases the user will need to manually authenticate the extension |
| 84 | ++ // through the UI) or the user has tokens for other accounts. |
| 85 | ++ if (existing?.content) { |
| 86 | ++ existing.content = existing.content.filter((existingToken) => { |
| 87 | ++ const scopes = existingToken.scopes.sort(); |
| 88 | ++ return !(tokens.find((token) => { |
| 89 | ++ return arrayEquals(scopes, token.scopes) |
| 90 | ++ && token.account?.label === existingToken.account?.label; |
| 91 | ++ })) |
102 | 92 | + })
|
103 |
| - } |
104 |
| - } |
105 |
| - |
| 93 | ++ } |
| 94 | ++ |
| 95 | ++ return this.setPassword(service, account, JSON.stringify({ |
| 96 | ++ extensionId, |
| 97 | ++ ...(existing || {}), |
| 98 | ++ content: JSON.stringify([ |
| 99 | ++ ...tokens, |
| 100 | ++ ...(existing?.content || []), |
| 101 | ++ ]) |
| 102 | ++ })); |
| 103 | ++ } |
| 104 | + } |
0 commit comments