mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 09:35:37 +00:00
feat: completed github org sync
This commit is contained in:
Vendored
+3
@@ -15,6 +15,7 @@ import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dy
|
||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
|
||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
|
||||
@@ -140,6 +141,7 @@ declare module "fastify" {
|
||||
passportUser: {
|
||||
isUserCompleted: boolean;
|
||||
providerAuthToken: string;
|
||||
externalProviderAccessToken?: string;
|
||||
};
|
||||
kmipUser: {
|
||||
projectId: string;
|
||||
@@ -244,6 +246,7 @@ declare module "fastify" {
|
||||
gateway: TGatewayServiceFactory;
|
||||
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||
githubOrgSync: TGithubOrgSyncServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
||||
Vendored
+9
-1
@@ -83,6 +83,9 @@ import {
|
||||
TGitAppOrg,
|
||||
TGitAppOrgInsert,
|
||||
TGitAppOrgUpdate,
|
||||
TGithubOrgSyncConfigs,
|
||||
TGithubOrgSyncConfigsInsert,
|
||||
TGithubOrgSyncConfigsUpdate,
|
||||
TGroupProjectMembershipRoles,
|
||||
TGroupProjectMembershipRolesInsert,
|
||||
TGroupProjectMembershipRolesUpdate,
|
||||
@@ -1004,5 +1007,10 @@ declare module "knex/types/tables" {
|
||||
TSecretReminderRecipientsInsert,
|
||||
TSecretReminderRecipientsUpdate
|
||||
>;
|
||||
[TableName.GithubOrgSyncConfig]: KnexOriginal.CompositeTableType<
|
||||
TGithubOrgSyncConfigs,
|
||||
TGithubOrgSyncConfigsInsert,
|
||||
TGithubOrgSyncConfigsUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasTable = await knex.schema.hasTable(TableName.GithubOrgSyncConfig);
|
||||
if (!hasTable) {
|
||||
await knex.schema.createTable(TableName.GithubOrgSyncConfig, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("githubOrgName").notNullable();
|
||||
t.boolean("isActive").defaultTo(false);
|
||||
t.binary("encryptedGithubOrgAccessToken");
|
||||
t.uuid("orgId").notNullable().unique();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.GithubOrgSyncConfig);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.GithubOrgSyncConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.GithubOrgSyncConfig);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const GithubOrgSyncConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
githubOrgName: z.string(),
|
||||
isActive: z.boolean().default(false).nullable().optional(),
|
||||
encryptedGithubOrgAccessToken: zodBuffer.nullable().optional(),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TGithubOrgSyncConfigs = z.infer<typeof GithubOrgSyncConfigsSchema>;
|
||||
export type TGithubOrgSyncConfigsInsert = Omit<z.input<typeof GithubOrgSyncConfigsSchema>, TImmutableDBKeys>;
|
||||
export type TGithubOrgSyncConfigsUpdate = Partial<Omit<z.input<typeof GithubOrgSyncConfigsSchema>, TImmutableDBKeys>>;
|
||||
@@ -25,6 +25,7 @@ export * from "./external-kms";
|
||||
export * from "./gateways";
|
||||
export * from "./git-app-install-sessions";
|
||||
export * from "./git-app-org";
|
||||
export * from "./github-org-sync-configs";
|
||||
export * from "./group-project-membership-roles";
|
||||
export * from "./group-project-memberships";
|
||||
export * from "./groups";
|
||||
|
||||
@@ -147,7 +147,8 @@ export enum TableName {
|
||||
KmipClientCertificates = "kmip_client_certificates",
|
||||
SecretRotationV2 = "secret_rotations_v2",
|
||||
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings",
|
||||
SecretReminderRecipients = "secret_reminder_recipients"
|
||||
SecretReminderRecipients = "secret_reminder_recipients",
|
||||
GithubOrgSyncConfig = "github_org_sync_configs"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { GithubOrgSyncConfigsSchema } from "@app/db/schemas";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const SanitiziedGithubOrgSyncSchema = GithubOrgSyncConfigsSchema.pick({
|
||||
isActive: true,
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
orgId: true,
|
||||
githubOrgName: true
|
||||
});
|
||||
|
||||
export const registerGithubOrgSyncRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
body: z.object({
|
||||
githubOrgName: z.string().trim(),
|
||||
githubOrgAccessToken: z.string().trim().max(1000).optional(),
|
||||
isActive: z.boolean().default(false)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
githubOrgSyncConfig: SanitiziedGithubOrgSyncSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const githubOrgSyncConfig = await server.services.githubOrgSync.createGithubOrgSync({
|
||||
orgPermission: req.permission,
|
||||
githubOrgName: req.body.githubOrgName,
|
||||
githubOrgAccessToken: req.body.githubOrgAccessToken,
|
||||
isActive: req.body.isActive
|
||||
});
|
||||
|
||||
return { githubOrgSyncConfig };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "PATCH",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
body: z
|
||||
.object({
|
||||
githubOrgName: z.string().trim(),
|
||||
githubOrgAccessToken: z.string().trim().max(1000),
|
||||
isActive: z.boolean().default(false)
|
||||
})
|
||||
.partial(),
|
||||
response: {
|
||||
200: z.object({
|
||||
githubOrgSyncConfig: SanitiziedGithubOrgSyncSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const githubOrgSyncConfig = await server.services.githubOrgSync.updateGithubOrgSync({
|
||||
orgPermission: req.permission,
|
||||
githubOrgName: req.body.githubOrgName,
|
||||
githubOrgAccessToken: req.body.githubOrgAccessToken,
|
||||
isActive: req.body.isActive
|
||||
});
|
||||
|
||||
return { githubOrgSyncConfig };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "DELETE",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
githubOrgSyncConfig: SanitiziedGithubOrgSyncSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const githubOrgSyncConfig = await server.services.githubOrgSync.deleteGithubOrgSync({
|
||||
orgPermission: req.permission
|
||||
});
|
||||
|
||||
return { githubOrgSyncConfig };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
githubOrgSyncConfig: SanitiziedGithubOrgSyncSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const githubOrgSyncConfig = await server.services.githubOrgSync.getGithubOrgSync({
|
||||
orgPermission: req.permission
|
||||
});
|
||||
|
||||
return { githubOrgSyncConfig };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"
|
||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||
import { registerExternalKmsRouter } from "./external-kms-router";
|
||||
import { registerGatewayRouter } from "./gateway-router";
|
||||
import { registerGithubOrgSyncRouter } from "./github-org-sync-router";
|
||||
import { registerGroupRouter } from "./group-router";
|
||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||
import { registerKmipRouter } from "./kmip-router";
|
||||
@@ -72,6 +73,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
);
|
||||
|
||||
await server.register(registerGatewayRouter, { prefix: "/gateways" });
|
||||
await server.register(registerGithubOrgSyncRouter, { prefix: "/github-org-sync-config" });
|
||||
|
||||
await server.register(
|
||||
async (pkiRouter) => {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TGithubOrgSyncDALFactory = ReturnType<typeof githubOrgSyncDALFactory>;
|
||||
|
||||
export const githubOrgSyncDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.GithubOrgSyncConfig);
|
||||
return orm;
|
||||
};
|
||||
@@ -0,0 +1,344 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Octokit } from "@octokit/core";
|
||||
import { paginateGraphQL } from "@octokit/plugin-paginate-graphql";
|
||||
import { Octokit as OctokitRest } from "@octokit/rest";
|
||||
|
||||
import { OrgMembershipRole } from "@app/db/schemas";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TGroupDALFactory } from "../group/group-dal";
|
||||
import { TUserGroupMembershipDALFactory } from "../group/user-group-membership-dal";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TGithubOrgSyncDALFactory } from "./github-org-sync-dal";
|
||||
import { TCreateGithubOrgSyncDTO, TDeleteGithubOrgSyncDTO, TUpdateGithubOrgSyncDTO } from "./github-org-sync-types";
|
||||
|
||||
const OctokitWithPlugin = Octokit.plugin(paginateGraphQL);
|
||||
|
||||
type TGithubOrgSyncServiceFactoryDep = {
|
||||
githubOrgSyncDAL: TGithubOrgSyncDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
"findGroupMembershipsByUserIdInOrg" | "insertMany" | "delete"
|
||||
>;
|
||||
groupDAL: Pick<TGroupDALFactory, "insertMany" | "transaction" | "find">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TGithubOrgSyncServiceFactory = ReturnType<typeof githubOrgSyncServiceFactory>;
|
||||
|
||||
export const githubOrgSyncServiceFactory = ({
|
||||
githubOrgSyncDAL,
|
||||
permissionService,
|
||||
kmsService,
|
||||
userGroupMembershipDAL,
|
||||
groupDAL,
|
||||
licenseService
|
||||
}: TGithubOrgSyncServiceFactoryDep) => {
|
||||
const createGithubOrgSync = async ({
|
||||
githubOrgName,
|
||||
orgPermission,
|
||||
githubOrgAccessToken,
|
||||
isActive
|
||||
}: TCreateGithubOrgSyncDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync);
|
||||
const plan = await licenseService.getPlan(orgPermission.orgId);
|
||||
if (!plan.githubOrgSync) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to create github organization team sync due to plan restriction. Upgrade plan to create github organization sync."
|
||||
});
|
||||
}
|
||||
|
||||
const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (existingConfig)
|
||||
throw new BadRequestError({
|
||||
message: `Organization ${orgPermission.orgId} already has GitHub Organization sync config.`
|
||||
});
|
||||
|
||||
const octokit = new OctokitRest({
|
||||
auth: githubOrgAccessToken,
|
||||
request: {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
}
|
||||
});
|
||||
const { data } = await octokit.rest.orgs.get({
|
||||
org: githubOrgName
|
||||
});
|
||||
if (data.login.toLowerCase() !== githubOrgName)
|
||||
throw new BadRequestError({ message: "Invalid GitHub organisation" });
|
||||
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: orgPermission.orgId
|
||||
});
|
||||
|
||||
const config = await githubOrgSyncDAL.create({
|
||||
orgId: orgPermission.orgId,
|
||||
githubOrgName,
|
||||
isActive,
|
||||
encryptedGithubOrgAccessToken: githubOrgAccessToken
|
||||
? encryptor({ plainText: Buffer.from(githubOrgAccessToken) }).cipherTextBlob
|
||||
: null
|
||||
});
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
const updateGithubOrgSync = async ({
|
||||
githubOrgName,
|
||||
orgPermission,
|
||||
githubOrgAccessToken,
|
||||
isActive
|
||||
}: TUpdateGithubOrgSyncDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync);
|
||||
const plan = await licenseService.getPlan(orgPermission.orgId);
|
||||
if (!plan.githubOrgSync) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to update github organization team sync due to plan restriction. Upgrade plan to update github organization sync."
|
||||
});
|
||||
}
|
||||
|
||||
const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!existingConfig)
|
||||
throw new BadRequestError({
|
||||
message: `Organization ${orgPermission.orgId} GitHub organization sync config missing.`
|
||||
});
|
||||
|
||||
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: orgPermission.orgId
|
||||
});
|
||||
const newData = {
|
||||
githubOrgName: githubOrgName || existingConfig.githubOrgName,
|
||||
githubOrgAccessToken:
|
||||
githubOrgAccessToken ||
|
||||
(existingConfig.encryptedGithubOrgAccessToken
|
||||
? decryptor({ cipherTextBlob: existingConfig.encryptedGithubOrgAccessToken }).toString()
|
||||
: null)
|
||||
};
|
||||
|
||||
if (githubOrgName || githubOrgAccessToken) {
|
||||
const octokit = new OctokitRest({
|
||||
auth: newData.githubOrgAccessToken,
|
||||
request: {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
}
|
||||
});
|
||||
const { data } = await octokit.rest.orgs.get({
|
||||
org: newData.githubOrgName
|
||||
});
|
||||
|
||||
if (data.login.toLowerCase() !== newData.githubOrgName)
|
||||
throw new BadRequestError({ message: "Invalid GitHub organisation" });
|
||||
}
|
||||
|
||||
const config = await githubOrgSyncDAL.updateById(existingConfig.id, {
|
||||
orgId: orgPermission.orgId,
|
||||
githubOrgName: newData.githubOrgName,
|
||||
isActive,
|
||||
encryptedGithubOrgAccessToken: newData.githubOrgAccessToken
|
||||
? encryptor({ plainText: Buffer.from(newData.githubOrgAccessToken) }).cipherTextBlob
|
||||
: null
|
||||
});
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
const deleteGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync);
|
||||
|
||||
const plan = await licenseService.getPlan(orgPermission.orgId);
|
||||
if (!plan.githubOrgSync) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to delete github organization team sync due to plan restriction. Upgrade plan to delete github organization sync."
|
||||
});
|
||||
}
|
||||
|
||||
const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!existingConfig)
|
||||
throw new BadRequestError({
|
||||
message: `Organization ${orgPermission.orgId} GitHub organization sync config missing.`
|
||||
});
|
||||
|
||||
const config = await githubOrgSyncDAL.deleteById(existingConfig.id);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
const getGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync);
|
||||
|
||||
const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!existingConfig)
|
||||
throw new NotFoundError({
|
||||
message: `Organization ${orgPermission.orgId} GitHub organization sync config missing.`
|
||||
});
|
||||
|
||||
return existingConfig;
|
||||
};
|
||||
|
||||
const syncUserGroups = async (orgId: string, userId: string, accessToken: string) => {
|
||||
const config = await githubOrgSyncDAL.findOne({ orgId });
|
||||
if (!config || !config?.isActive) return;
|
||||
|
||||
const infisicalUserGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(userId, orgId);
|
||||
const infisicalUserGroupSet = new Set(infisicalUserGroups.map((el) => el.groupName));
|
||||
|
||||
const octoRest = new OctokitRest({
|
||||
auth: accessToken,
|
||||
request: {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
}
|
||||
});
|
||||
const { data: userDetails } = await octoRest.rest.users.getAuthenticated();
|
||||
|
||||
const octokit = new OctokitWithPlugin({
|
||||
auth: accessToken,
|
||||
request: {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
}
|
||||
});
|
||||
const data = await octokit.graphql
|
||||
.paginate<{
|
||||
organization: { teams: { totalCount: number; edges: { node: { name: string; description: string } }[] } };
|
||||
}>(
|
||||
`
|
||||
query orgTeams($cursor: String,$org: String!, $username: String!){
|
||||
organization(login: $org) {
|
||||
teams(first: 100, userLogins: [$username], after: $cursor) {
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
org: config.githubOrgName,
|
||||
username: userDetails.login
|
||||
}
|
||||
)
|
||||
.catch((err) => {
|
||||
if ((err as Error)?.message?.includes("Although you appear to have the correct authorization credential")) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Please check your organization have approved Infisical Oauth application. For more info: https://infisical.com/docs/documentation/platform/github-org-sync#troubleshooting"
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({ message: (err as Error)?.message });
|
||||
});
|
||||
|
||||
const {
|
||||
organization: { teams }
|
||||
} = data;
|
||||
const githubUserTeams = teams?.edges?.map((el) => el.node.name.toLowerCase());
|
||||
const githubUserTeamSet = new Set(githubUserTeams);
|
||||
const githubUserTeamOnInfisical = await groupDAL.find({ orgId, $in: { name: githubUserTeams } });
|
||||
const githubUserTeamOnInfisicalGroupByName = groupBy(githubUserTeamOnInfisical, (i) => i.name);
|
||||
|
||||
const newTeams = githubUserTeams.filter(
|
||||
(el) => !infisicalUserGroupSet.has(el) && !Object.hasOwn(githubUserTeamOnInfisicalGroupByName, el)
|
||||
);
|
||||
const updateTeams = githubUserTeams.filter(
|
||||
(el) => !infisicalUserGroupSet.has(el) && Object.hasOwn(githubUserTeamOnInfisicalGroupByName, el)
|
||||
);
|
||||
const removeFromTeams = infisicalUserGroups.filter((el) => !githubUserTeamSet.has(el.groupName));
|
||||
|
||||
if (newTeams.length || updateTeams.length || removeFromTeams.length) {
|
||||
await groupDAL.transaction(async (tx) => {
|
||||
if (newTeams.length) {
|
||||
const newGroups = await groupDAL.insertMany(
|
||||
newTeams.map((newGroupName) => ({
|
||||
name: newGroupName,
|
||||
role: OrgMembershipRole.Member,
|
||||
slug: newGroupName,
|
||||
orgId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await userGroupMembershipDAL.insertMany(
|
||||
newGroups.map((el) => ({
|
||||
groupId: el.id,
|
||||
userId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (updateTeams.length) {
|
||||
await userGroupMembershipDAL.insertMany(
|
||||
updateTeams.map((el) => ({
|
||||
groupId: githubUserTeamOnInfisicalGroupByName[el][0].id,
|
||||
userId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (removeFromTeams.length) {
|
||||
await userGroupMembershipDAL.delete(
|
||||
{ userId, $in: { groupId: removeFromTeams.map((el) => el.groupId) } },
|
||||
tx
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
createGithubOrgSync,
|
||||
updateGithubOrgSync,
|
||||
deleteGithubOrgSync,
|
||||
getGithubOrgSync,
|
||||
syncUserGroups
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
export interface TCreateGithubOrgSyncDTO {
|
||||
orgPermission: OrgServiceActor;
|
||||
githubOrgName: string;
|
||||
githubOrgAccessToken?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface TUpdateGithubOrgSyncDTO {
|
||||
orgPermission: OrgServiceActor;
|
||||
githubOrgName?: string;
|
||||
githubOrgAccessToken?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface TDeleteGithubOrgSyncDTO {
|
||||
orgPermission: OrgServiceActor;
|
||||
}
|
||||
|
||||
export interface TGetGithubOrgSyncDTO {
|
||||
orgPermission: OrgServiceActor;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
pitRecovery: false,
|
||||
ipAllowlisting: false,
|
||||
rbac: false,
|
||||
githubOrgSync: false,
|
||||
customRateLimits: false,
|
||||
customAlerts: false,
|
||||
secretAccessInsights: false,
|
||||
|
||||
@@ -45,6 +45,7 @@ export type TFeatureSet = {
|
||||
auditLogsRetentionDays: 0;
|
||||
auditLogStreams: false;
|
||||
auditLogStreamLimit: 3;
|
||||
githubOrgSync: false;
|
||||
samlSSO: false;
|
||||
hsm: false;
|
||||
oidcSSO: false;
|
||||
|
||||
@@ -74,6 +74,7 @@ export enum OrgPermissionSubjects {
|
||||
IncidentAccount = "incident-contact",
|
||||
Sso = "sso",
|
||||
Scim = "scim",
|
||||
GithubOrgSync = "github-org-sync",
|
||||
Ldap = "ldap",
|
||||
Groups = "groups",
|
||||
Billing = "billing",
|
||||
@@ -101,6 +102,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.GithubOrgSync]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
|
||||
| [OrgPermissionGroupActions, OrgPermissionSubjects.Groups]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
@@ -165,6 +167,10 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
||||
subject: z.literal(OrgPermissionSubjects.Scim).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(OrgPermissionSubjects.GithubOrgSync).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(OrgPermissionSubjects.Ldap).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||
@@ -273,6 +279,11 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN = "x-infisical-github-auth-access-token";
|
||||
@@ -33,6 +33,8 @@ import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
|
||||
import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal";
|
||||
import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal";
|
||||
import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal";
|
||||
import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
|
||||
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { groupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
@@ -421,6 +423,7 @@ export const registerRoutes = async (
|
||||
const gatewayDAL = gatewayDALFactory(db);
|
||||
const projectGatewayDAL = projectGatewayDALFactory(db);
|
||||
const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db);
|
||||
const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
|
||||
|
||||
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
|
||||
|
||||
@@ -558,6 +561,15 @@ export const registerRoutes = async (
|
||||
externalGroupOrgRoleMappingDAL
|
||||
});
|
||||
|
||||
const githubOrgSyncConfigService = githubOrgSyncServiceFactory({
|
||||
licenseService,
|
||||
githubOrgSyncDAL,
|
||||
kmsService,
|
||||
permissionService,
|
||||
groupDAL,
|
||||
userGroupMembershipDAL
|
||||
});
|
||||
|
||||
const ldapService = ldapConfigServiceFactory({
|
||||
ldapConfigDAL,
|
||||
ldapGroupMapDAL,
|
||||
@@ -1689,7 +1701,8 @@ export const registerRoutes = async (
|
||||
kmipOperation: kmipOperationService,
|
||||
gateway: gatewayService,
|
||||
secretRotationV2: secretRotationV2Service,
|
||||
assumePrivileges: assumePrivilegeService
|
||||
assumePrivileges: assumePrivilegeService,
|
||||
githubOrgSync: githubOrgSyncConfigService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
@@ -1750,30 +1763,6 @@ export const registerRoutes = async (
|
||||
|
||||
logger.info(`Raw event loop stats: ${JSON.stringify(histogram, null, 2)}`);
|
||||
|
||||
// try {
|
||||
// await db.raw("SELECT NOW()");
|
||||
// } catch (err) {
|
||||
// logger.error("Health check: database connection failed", err);
|
||||
// return reply.code(503).send({
|
||||
// date: new Date(),
|
||||
// message: "Service unavailable"
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (cfg.isRedisConfigured) {
|
||||
// const redis = new Redis(cfg.REDIS_URL);
|
||||
// try {
|
||||
// await redis.ping();
|
||||
// redis.disconnect();
|
||||
// } catch (err) {
|
||||
// logger.error("Health check: redis connection failed", err);
|
||||
// return reply.code(503).send({
|
||||
// date: new Date(),
|
||||
// message: "Service unavailable"
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
return {
|
||||
date: new Date(),
|
||||
message: "Ok",
|
||||
|
||||
@@ -14,9 +14,11 @@ import { Strategy as GitLabStrategy } from "passport-gitlab2";
|
||||
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
|
||||
import { z } from "zod";
|
||||
|
||||
import { INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN } from "@app/lib/config/const";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { fetchGithubEmails } from "@app/lib/requests/github";
|
||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||
@@ -95,7 +97,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
clientID: appCfg.CLIENT_ID_GITHUB_LOGIN as string,
|
||||
clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN as string,
|
||||
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/github`,
|
||||
scope: ["user:email"],
|
||||
scope: ["user:email", "read:org"],
|
||||
// akhilmhdh: because the ts type for this is outdated by the maintainer
|
||||
state: true as unknown as string
|
||||
},
|
||||
@@ -113,7 +115,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
authMethod: AuthMethod.GITHUB,
|
||||
callbackPort
|
||||
});
|
||||
return cb(null, { isUserCompleted, providerAuthToken });
|
||||
return cb(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
cb(error as Error, false);
|
||||
@@ -290,6 +292,17 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
}) as any,
|
||||
handler: async (req, res) => {
|
||||
await req.session.destroy();
|
||||
|
||||
if (req.passportUser.externalProviderAccessToken) {
|
||||
void res.cookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, req.passportUser.externalProviderAccessToken, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: appCfg.HTTPS_ENABLED,
|
||||
expires: new Date(Date.now() + ms(appCfg.JWT_PROVIDER_AUTH_LIFETIME))
|
||||
});
|
||||
}
|
||||
|
||||
if (req.passportUser.isUserCompleted) {
|
||||
return res.redirect(
|
||||
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN } from "@app/lib/config/const";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||
|
||||
@@ -70,6 +71,21 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
};
|
||||
}
|
||||
|
||||
const githubOauthAccessToken = req.cookies[INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN];
|
||||
if (githubOauthAccessToken) {
|
||||
await server.services.githubOrgSync
|
||||
.syncUserGroups(req.body.organizationId, tokens.user.userId, githubOauthAccessToken)
|
||||
.finally(() => {
|
||||
void res.setCookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, "", {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: cfg.HTTPS_ENABLED,
|
||||
maxAge: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void res.setCookie("jid", tokens.refresh, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
|
||||
@@ -476,6 +476,7 @@ export const authLoginServiceFactory = ({
|
||||
|
||||
return {
|
||||
...tokens,
|
||||
user,
|
||||
isMfaEnabled: false
|
||||
};
|
||||
};
|
||||
@@ -784,7 +785,7 @@ export const authLoginServiceFactory = ({
|
||||
organizationId
|
||||
});
|
||||
|
||||
return { token, isMfaEnabled: false, user: userEnc } as const;
|
||||
return { token, isMfaEnabled: false, user: userEnc, decodedProviderToken } as const;
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -177,6 +177,7 @@ export const deleteGithubSecrets = async ({
|
||||
selected_repositories_url?: string | undefined;
|
||||
}
|
||||
|
||||
// @ts-expect-error just octokit ts compatiability issue
|
||||
const OctokitWithRetry = Octokit.plugin(retry);
|
||||
let octokit: Octokit;
|
||||
const appCfg = getConfig();
|
||||
|
||||
Reference in New Issue
Block a user