Skip to content

Commit f8b8078

Browse files
authored
Setup Github teams for SIG admins (#312)
1 parent 1963016 commit f8b8078

File tree

13 files changed

+1357
-88
lines changed

13 files changed

+1357
-88
lines changed

src/api/functions/entraId.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,6 @@ export async function createM365Group(
852852
);
853853
}
854854

855-
console.log(createUrl, body);
856855
const createResponse = await fetch(createUrl, {
857856
method: "POST",
858857
headers: {

src/api/functions/github.ts

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import { ValidLoggers } from "api/types.js";
2+
import { sleep } from "api/utils.js";
3+
import { BaseError, GithubError } from "common/errors/index.js";
4+
import { Octokit } from "octokit";
5+
6+
export interface CreateGithubTeamInputs {
7+
githubToken: string;
8+
orgId: string;
9+
parentTeamId: number;
10+
name: string;
11+
description?: string;
12+
privacy?: "secret" | "closed";
13+
logger: ValidLoggers;
14+
groupsToSync?: string[];
15+
}
16+
17+
async function findIdpGroupWithRetry({
18+
octokit,
19+
orgId,
20+
groupId,
21+
logger,
22+
maxRetries,
23+
}: {
24+
octokit: Octokit;
25+
groupId: string;
26+
orgId: string;
27+
logger: ValidLoggers;
28+
maxRetries: number;
29+
}): Promise<{
30+
group_id: string;
31+
group_name: string;
32+
group_description: string;
33+
} | null> {
34+
for (let attempt = 0; attempt < maxRetries; attempt++) {
35+
try {
36+
logger.info(
37+
`Searching for IdP group ${groupId} (attempt ${attempt + 1}/${maxRetries})`,
38+
);
39+
40+
// List all IdP groups
41+
const response = await octokit.request(
42+
"GET /orgs/{org}/team-sync/groups",
43+
{
44+
org: orgId,
45+
headers: {
46+
"X-GitHub-Api-Version": "2022-11-28",
47+
},
48+
},
49+
);
50+
51+
// Search for the group by ID
52+
const group = response.data.groups?.find(
53+
(g: any) => g.group_id === groupId,
54+
);
55+
56+
if (group) {
57+
logger.info(`Found IdP group: ${group.group_name}`);
58+
return {
59+
group_id: group.group_id,
60+
group_name: group.group_name,
61+
group_description: group.group_description || "",
62+
};
63+
}
64+
65+
if (attempt < maxRetries - 1) {
66+
const baseDelay = 250;
67+
const exponentialDelay = baseDelay * 2 ** attempt;
68+
const jitter = Math.random() * 250;
69+
const delay = exponentialDelay + jitter;
70+
71+
logger.warn(
72+
`IdP group ${groupId} not found, retrying in ${Math.round(delay)}ms...`,
73+
);
74+
await sleep(delay);
75+
}
76+
} catch (error) {
77+
logger.error(
78+
`Error searching for IdP group (attempt ${attempt + 1}/${maxRetries}):`,
79+
error,
80+
);
81+
82+
if (attempt < maxRetries - 1) {
83+
const baseDelay = 1000;
84+
const exponentialDelay = baseDelay * 2 ** attempt;
85+
const jitter = Math.random() * 1000;
86+
const delay = exponentialDelay + jitter;
87+
88+
logger.warn(`Retrying in ${Math.round(delay)}ms...`);
89+
await sleep(delay);
90+
}
91+
}
92+
}
93+
94+
return null;
95+
}
96+
97+
export async function createGithubTeam({
98+
githubToken,
99+
orgId,
100+
parentTeamId,
101+
description,
102+
name,
103+
privacy,
104+
logger,
105+
}: Omit<CreateGithubTeamInputs, "groupsToSync">) {
106+
try {
107+
const octokit = new Octokit({
108+
auth: githubToken,
109+
});
110+
logger.info(`Checking if GitHub team "${name}" exists`);
111+
const teamsResponse = await octokit.request("GET /orgs/{org}/teams", {
112+
org: orgId,
113+
});
114+
115+
const existingTeam = teamsResponse.data.find(
116+
(team: { name: string; id: number }) => team.name === name,
117+
);
118+
119+
if (existingTeam) {
120+
logger.info(`Team "${name}" already exists with id: ${existingTeam.id}`);
121+
return existingTeam.id;
122+
}
123+
logger.info(`Creating GitHub team "${name}"`);
124+
const response = await octokit.request("POST /orgs/{org}/teams", {
125+
org: orgId,
126+
name,
127+
description: `[Managed by Core API]${description ? ` ${description}` : ""}`,
128+
privacy: privacy || "closed",
129+
notification_setting: "notifications_enabled",
130+
parent_team_id: parentTeamId,
131+
});
132+
if (response.status !== 201) {
133+
logger.error(response.data);
134+
throw new GithubError({
135+
message: "Failed to create Github team.",
136+
});
137+
}
138+
const newTeamSlug = response.data.slug;
139+
const newTeamId = response.data.id;
140+
logger.info(`Created Github Team with slug ${newTeamSlug}`);
141+
142+
// Remove the authenticated user from the team
143+
try {
144+
const { data: authenticatedUser } = await octokit.request("GET /user");
145+
logger.info(
146+
`Removing user ${authenticatedUser.login} from team ${newTeamId}`,
147+
);
148+
149+
await octokit.request(
150+
"DELETE /orgs/{org}/teams/{team_id}/memberships/{username}",
151+
{
152+
org: orgId,
153+
team_id: newTeamId,
154+
username: authenticatedUser.login,
155+
},
156+
);
157+
158+
logger.info(
159+
`Successfully removed ${authenticatedUser.login} from team ${newTeamId}`,
160+
);
161+
} catch (removeError) {
162+
logger.warn(`Failed to remove user from team ${newTeamId}:`, removeError);
163+
// Don't throw here - team was created successfully
164+
}
165+
166+
return newTeamId;
167+
} catch (e) {
168+
if (e instanceof BaseError) {
169+
throw e;
170+
}
171+
logger.error("Failed to create GitHub team.");
172+
logger.error(e);
173+
throw new GithubError({
174+
message: "Failed to create GitHub team.",
175+
});
176+
}
177+
}
178+
179+
export async function assignIdpGroupsToTeam({
180+
githubToken,
181+
teamId,
182+
groupsToSync,
183+
logger,
184+
orgId,
185+
}: {
186+
githubToken: string;
187+
teamId: number;
188+
groupsToSync: string[];
189+
logger: ValidLoggers;
190+
orgId: string;
191+
}) {
192+
try {
193+
const octokit = new Octokit({
194+
auth: githubToken,
195+
});
196+
197+
if (!groupsToSync || groupsToSync.length === 0) {
198+
logger.info("No IdP groups to sync");
199+
return;
200+
}
201+
202+
// Search for IdP groups with retry logic
203+
const idpGroups = [];
204+
for (const groupId of groupsToSync) {
205+
const idpGroup = await findIdpGroupWithRetry({
206+
octokit,
207+
orgId,
208+
groupId,
209+
logger,
210+
maxRetries: 5,
211+
});
212+
213+
if (!idpGroup) {
214+
logger.error(
215+
`Failed to find IdP group with ID ${groupId} after 5 retries`,
216+
);
217+
throw new GithubError({
218+
message: `IdP group with ID ${groupId} not found`,
219+
});
220+
}
221+
idpGroups.push(idpGroup);
222+
}
223+
224+
// Add IdP group mappings to team
225+
logger.info(`Mapping ${idpGroups.length} IdP group(s) to team ${teamId}`);
226+
await octokit.request(
227+
"PATCH /orgs/{org}/teams/{team_id}/team-sync/group-mappings",
228+
{
229+
org: orgId,
230+
team_id: teamId,
231+
groups: idpGroups,
232+
headers: {
233+
"X-GitHub-Api-Version": "2022-11-28",
234+
},
235+
},
236+
);
237+
238+
logger.info(`Successfully mapped IdP groups to team ${teamId}`);
239+
} catch (e) {
240+
if (e instanceof BaseError) {
241+
throw e;
242+
}
243+
logger.error(`Failed to assign IdP groups to team ${teamId}`);
244+
logger.error(e);
245+
throw new GithubError({
246+
message: `Failed to assign IdP groups to team ${teamId}`,
247+
});
248+
}
249+
}

src/api/functions/organizations.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,18 @@ import { EntraGroupActions } from "common/types/iam.js";
2323
import { buildAuditLogTransactPut } from "./auditLog.js";
2424
import { Modules } from "common/modules.js";
2525
import { retryDynamoTransactionWithBackoff } from "api/utils.js";
26+
import { ValidLoggers } from "api/types.js";
2627

2728
export interface GetOrgInfoInputs {
2829
id: string;
2930
dynamoClient: DynamoDBClient;
30-
logger: FastifyBaseLogger | pino.Logger;
31+
logger: ValidLoggers;
3132
}
3233

3334
export interface GetUserOrgRolesInputs {
3435
username: string;
3536
dynamoClient: DynamoDBClient;
36-
logger: FastifyBaseLogger | pino.Logger;
37+
logger: ValidLoggers;
3738
}
3839

3940
export type SQSMessage = Record<any, any>;

src/api/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"moment": "^2.30.1",
5353
"moment-timezone": "^0.6.0",
5454
"node-cache": "^5.1.2",
55+
"octokit": "^5.0.3",
5556
"passkit-generator": "^3.3.1",
5657
"pino": "^9.6.0",
5758
"pluralize": "^8.0.0",
@@ -72,4 +73,4 @@
7273
"pino-pretty": "^13.1.1",
7374
"yaml": "^2.8.1"
7475
}
75-
}
76+
}

0 commit comments

Comments
 (0)