Skip to content

Commit 1cd9007

Browse files
committed
github app service auth
1 parent 1360caa commit 1cd9007

File tree

13 files changed

+765
-10
lines changed

13 files changed

+765
-10
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
{/* THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! */}
2+
```json
3+
{
4+
"$schema": "http://json-schema.org/draft-07/schema#",
5+
"title": "AppConfig",
6+
"oneOf": [
7+
{
8+
"$schema": "http://json-schema.org/draft-07/schema#",
9+
"type": "object",
10+
"title": "GithubAppConfig",
11+
"properties": {
12+
"type": {
13+
"const": "githubApp",
14+
"description": "GitHub App Configuration"
15+
},
16+
"deploymentHostname": {
17+
"type": "string",
18+
"format": "url",
19+
"default": "github.com",
20+
"description": "The hostname of the GitHub App deployment.",
21+
"examples": [
22+
"github.com",
23+
"github.example.com"
24+
],
25+
"pattern": "^[^\\s/$.?#].[^\\s]*$"
26+
},
27+
"id": {
28+
"type": "string",
29+
"description": "The ID of the GitHub App."
30+
},
31+
"privateKey": {
32+
"description": "The private key of the GitHub App.",
33+
"anyOf": [
34+
{
35+
"type": "object",
36+
"properties": {
37+
"secret": {
38+
"type": "string",
39+
"description": "The name of the secret that contains the token."
40+
}
41+
},
42+
"required": [
43+
"secret"
44+
],
45+
"additionalProperties": false
46+
},
47+
{
48+
"type": "object",
49+
"properties": {
50+
"env": {
51+
"type": "string",
52+
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
53+
}
54+
},
55+
"required": [
56+
"env"
57+
],
58+
"additionalProperties": false
59+
}
60+
]
61+
},
62+
"privateKeyPath": {
63+
"description": "The path to the private key of the GitHub App.",
64+
"anyOf": [
65+
{
66+
"type": "object",
67+
"properties": {
68+
"secret": {
69+
"type": "string",
70+
"description": "The name of the secret that contains the token."
71+
}
72+
},
73+
"required": [
74+
"secret"
75+
],
76+
"additionalProperties": false
77+
},
78+
{
79+
"type": "object",
80+
"properties": {
81+
"env": {
82+
"type": "string",
83+
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
84+
}
85+
},
86+
"required": [
87+
"env"
88+
],
89+
"additionalProperties": false
90+
}
91+
]
92+
}
93+
},
94+
"required": [
95+
"type",
96+
"id"
97+
],
98+
"oneOf": [
99+
{
100+
"required": [
101+
"privateKey"
102+
]
103+
},
104+
{
105+
"required": [
106+
"privateKeyPath"
107+
]
108+
}
109+
],
110+
"additionalProperties": false
111+
}
112+
]
113+
}
114+
```
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
{/* THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! */}
2+
```json
3+
{
4+
"$schema": "http://json-schema.org/draft-07/schema#",
5+
"type": "object",
6+
"title": "GithubAppConfig",
7+
"properties": {
8+
"type": {
9+
"const": "githubApp",
10+
"description": "GitHub App Configuration"
11+
},
12+
"deploymentHostname": {
13+
"type": "string",
14+
"format": "url",
15+
"default": "github.com",
16+
"description": "The hostname of the GitHub App deployment.",
17+
"examples": [
18+
"github.com",
19+
"github.example.com"
20+
],
21+
"pattern": "^[^\\s/$.?#].[^\\s]*$"
22+
},
23+
"id": {
24+
"type": "string",
25+
"description": "The ID of the GitHub App."
26+
},
27+
"privateKey": {
28+
"description": "The private key of the GitHub App.",
29+
"anyOf": [
30+
{
31+
"type": "object",
32+
"properties": {
33+
"secret": {
34+
"type": "string",
35+
"description": "The name of the secret that contains the token."
36+
}
37+
},
38+
"required": [
39+
"secret"
40+
],
41+
"additionalProperties": false
42+
},
43+
{
44+
"type": "object",
45+
"properties": {
46+
"env": {
47+
"type": "string",
48+
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
49+
}
50+
},
51+
"required": [
52+
"env"
53+
],
54+
"additionalProperties": false
55+
}
56+
]
57+
},
58+
"privateKeyPath": {
59+
"description": "The path to the private key of the GitHub App.",
60+
"anyOf": [
61+
{
62+
"type": "object",
63+
"properties": {
64+
"secret": {
65+
"type": "string",
66+
"description": "The name of the secret that contains the token."
67+
}
68+
},
69+
"required": [
70+
"secret"
71+
],
72+
"additionalProperties": false
73+
},
74+
{
75+
"type": "object",
76+
"properties": {
77+
"env": {
78+
"type": "string",
79+
"description": "The name of the environment variable that contains the token. Only supported in declarative connection configs."
80+
}
81+
},
82+
"required": [
83+
"env"
84+
],
85+
"additionalProperties": false
86+
}
87+
]
88+
}
89+
},
90+
"required": [
91+
"type",
92+
"id"
93+
],
94+
"oneOf": [
95+
{
96+
"required": [
97+
"privateKey"
98+
]
99+
},
100+
{
101+
"required": [
102+
"privateKeyPath"
103+
]
104+
}
105+
],
106+
"additionalProperties": false
107+
}
108+
```
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { GithubAppConfig, SourcebotConfig } from "@sourcebot/schemas/v3/index.type";
2+
import { loadConfig } from "@sourcebot/shared";
3+
import { env } from "../env.js";
4+
import { createLogger } from "@sourcebot/logger";
5+
import { getTokenFromConfig } from "../utils.js";
6+
import { PrismaClient } from "@sourcebot/db";
7+
import { App } from "@octokit/app";
8+
9+
const logger = createLogger('githubAppManager');
10+
const GITHUB_DEFAULT_DEPLOYMENT_HOSTNAME = 'github.com';
11+
12+
type Installation = {
13+
id: number;
14+
appId: number;
15+
account: {
16+
login: string;
17+
type: 'organization' | 'user';
18+
};
19+
createdAt: string;
20+
expiresAt: string;
21+
token: string;
22+
};
23+
24+
export class GithubAppManager {
25+
private static instance: GithubAppManager | null = null;
26+
private octokitApps: Map<number, App>;
27+
private installationMap: Map<string, Installation>;
28+
private db: PrismaClient | null = null;
29+
private initialized: boolean = false;
30+
31+
private constructor() {
32+
this.octokitApps = new Map<number, App>();
33+
this.installationMap = new Map<string, Installation>();
34+
}
35+
36+
public static getInstance(): GithubAppManager {
37+
if (!GithubAppManager.instance) {
38+
GithubAppManager.instance = new GithubAppManager();
39+
}
40+
return GithubAppManager.instance;
41+
}
42+
43+
private ensureInitialized(): void {
44+
if (!this.initialized) {
45+
throw new Error('GithubAppManager must be initialized before use. Call init() first.');
46+
}
47+
}
48+
49+
public async init(db: PrismaClient) {
50+
this.db = db;
51+
const config = await loadConfig(env.CONFIG_PATH!);
52+
const githubApps = config.apps?.filter(app => app.type === 'githubApp') as GithubAppConfig[];
53+
54+
logger.info(`Found ${githubApps.length} GitHub apps in config`);
55+
56+
for (const app of githubApps) {
57+
const deploymentHostname = app.deploymentHostname as string || GITHUB_DEFAULT_DEPLOYMENT_HOSTNAME;
58+
59+
// @todo: we should move SINGLE_TENANT_ORG_ID to shared package or just remove the need to pass this in
60+
// when resolving tokens
61+
const SINGLE_TENANT_ORG_ID = 1;
62+
const privateKey = await getTokenFromConfig(app.privateKey, SINGLE_TENANT_ORG_ID, this.db!);
63+
64+
const octokitApp = new App({
65+
appId: Number(app.id),
66+
privateKey: privateKey,
67+
});
68+
this.octokitApps.set(Number(app.id), octokitApp);
69+
70+
const installations = await octokitApp.octokit.request("GET /app/installations");
71+
logger.info(`Found ${installations.data.length} GitHub App installations for ${deploymentHostname}/${app.id}:`);
72+
73+
for (const installationData of installations.data) {
74+
logger.info(`\tInstallation ID: ${installationData.id}, Account: ${installationData.account?.login}, Type: ${installationData.account?.type}`);
75+
76+
const owner = installationData.account?.login!;
77+
const accountType = installationData.account?.type!.toLowerCase() as 'organization' | 'user';
78+
const installationOctokit = await octokitApp.getInstallationOctokit(installationData.id);
79+
const auth = await installationOctokit.auth({ type: "installation" }) as { expires_at: string, token: string };
80+
81+
const installation: Installation = {
82+
id: installationData.id,
83+
appId: Number(app.id),
84+
account: {
85+
login: owner,
86+
type: accountType,
87+
},
88+
createdAt: installationData.created_at,
89+
expiresAt: auth.expires_at,
90+
token: auth.token
91+
};
92+
this.installationMap.set(this.generateMapKey(owner, deploymentHostname), installation);
93+
}
94+
}
95+
96+
this.initialized = true;
97+
}
98+
99+
public async getInstallationToken(owner: string, deploymentHostname: string = GITHUB_DEFAULT_DEPLOYMENT_HOSTNAME): Promise<string> {
100+
this.ensureInitialized();
101+
102+
const key = this.generateMapKey(owner, deploymentHostname);
103+
const installation = this.installationMap.get(key) as Installation | undefined;
104+
if (!installation) {
105+
throw new Error(`GitHub App Installation not found for ${key}`);
106+
}
107+
108+
if (installation.expiresAt < new Date().toISOString()) {
109+
const octokitApp = this.octokitApps.get(installation.appId) as App;
110+
const installationOctokit = await octokitApp.getInstallationOctokit(installation.id);
111+
const auth = await installationOctokit.auth({ type: "installation" }) as { expires_at: string, token: string };
112+
113+
const newInstallation: Installation = {
114+
...installation,
115+
expiresAt: auth.expires_at,
116+
token: auth.token
117+
};
118+
this.installationMap.set(key, newInstallation);
119+
120+
return newInstallation.token;
121+
} else {
122+
return installation.token;
123+
}
124+
}
125+
126+
public appsConfigured() {
127+
return this.octokitApps.size > 0;
128+
}
129+
130+
private generateMapKey(owner: string, deploymentHostname: string): string {
131+
return `${deploymentHostname}/${owner}`;
132+
}
133+
}

packages/backend/src/ee/repoPermissionSyncer.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ const QUEUE_NAME = 'repoPermissionSyncQueue';
1818

1919
const logger = createLogger('repo-permission-syncer');
2020

21-
2221
export class RepoPermissionSyncer {
2322
private queue: Queue<RepoPermissionSyncJob>;
2423
private worker: Worker<RepoPermissionSyncJob>;

0 commit comments

Comments
 (0)