Merge remote-tracking branch 'origin/main' into feat/gitlabSecretSync

This commit is contained in:
carlosmonastyrski
2025-06-24 22:42:28 -03:00
213 changed files with 8222 additions and 1633 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ import { TAuditLogServiceFactory, TCreateAuditLogDTO } from "@app/ee/services/au
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-types";
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-types";
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-types";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-types";
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";
+4 -1
View File
@@ -50,6 +50,8 @@ export const initDbConnection = ({
}
: false
},
// https://knexjs.org/guide/#pool
pool: { min: 0, max: 10 },
migrations: {
tableName: "infisical_migrations"
}
@@ -70,7 +72,8 @@ export const initDbConnection = ({
},
migrations: {
tableName: "infisical_migrations"
}
},
pool: { min: 0, max: 10 }
});
});
@@ -0,0 +1,91 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasEncryptedGithubAppConnectionClientIdColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionClientId"
);
const hasEncryptedGithubAppConnectionClientSecretColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionClientSecret"
);
const hasEncryptedGithubAppConnectionSlugColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionSlug"
);
const hasEncryptedGithubAppConnectionAppIdColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionId"
);
const hasEncryptedGithubAppConnectionAppPrivateKeyColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionPrivateKey"
);
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
if (!hasEncryptedGithubAppConnectionClientIdColumn) {
t.binary("encryptedGitHubAppConnectionClientId").nullable();
}
if (!hasEncryptedGithubAppConnectionClientSecretColumn) {
t.binary("encryptedGitHubAppConnectionClientSecret").nullable();
}
if (!hasEncryptedGithubAppConnectionSlugColumn) {
t.binary("encryptedGitHubAppConnectionSlug").nullable();
}
if (!hasEncryptedGithubAppConnectionAppIdColumn) {
t.binary("encryptedGitHubAppConnectionId").nullable();
}
if (!hasEncryptedGithubAppConnectionAppPrivateKeyColumn) {
t.binary("encryptedGitHubAppConnectionPrivateKey").nullable();
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasEncryptedGithubAppConnectionClientIdColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionClientId"
);
const hasEncryptedGithubAppConnectionClientSecretColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionClientSecret"
);
const hasEncryptedGithubAppConnectionSlugColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionSlug"
);
const hasEncryptedGithubAppConnectionAppIdColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionId"
);
const hasEncryptedGithubAppConnectionAppPrivateKeyColumn = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedGitHubAppConnectionPrivateKey"
);
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
if (hasEncryptedGithubAppConnectionClientIdColumn) {
t.dropColumn("encryptedGitHubAppConnectionClientId");
}
if (hasEncryptedGithubAppConnectionClientSecretColumn) {
t.dropColumn("encryptedGitHubAppConnectionClientSecret");
}
if (hasEncryptedGithubAppConnectionSlugColumn) {
t.dropColumn("encryptedGitHubAppConnectionSlug");
}
if (hasEncryptedGithubAppConnectionAppIdColumn) {
t.dropColumn("encryptedGitHubAppConnectionId");
}
if (hasEncryptedGithubAppConnectionAppPrivateKeyColumn) {
t.dropColumn("encryptedGitHubAppConnectionPrivateKey");
}
});
}
+6 -1
View File
@@ -29,7 +29,12 @@ export const SuperAdminSchema = z.object({
adminIdentityIds: z.string().array().nullable().optional(),
encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(),
encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(),
encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional()
encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionClientId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionClientSecret: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionSlug: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional()
});
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
@@ -89,7 +89,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
schema: {
querystring: z.object({
projectSlug: z.string().trim(),
authorProjectMembershipId: z.string().trim().optional(),
authorUserId: z.string().trim().optional(),
envSlug: z.string().trim().optional()
}),
response: {
@@ -143,7 +143,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
handler: async (req) => {
const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({
projectSlug: req.query.projectSlug,
authorProjectMembershipId: req.query.authorProjectMembershipId,
authorUserId: req.query.authorUserId,
envSlug: req.query.envSlug,
actor: req.permission.type,
actorId: req.permission.id,
@@ -30,6 +30,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
workspaceId: z.string().trim(),
environment: z.string().trim().optional(),
committer: z.string().trim().optional(),
search: z.string().trim().optional(),
status: z.nativeEnum(RequestState).optional(),
limit: z.coerce.number().default(20),
offset: z.coerce.number().default(0)
@@ -66,13 +67,14 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
userId: z.string().nullable().optional()
})
.array()
}).array()
}).array(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const approvals = await server.services.secretApprovalRequest.getSecretApprovals({
const { approvals, totalCount } = await server.services.secretApprovalRequest.getSecretApprovals({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -80,7 +82,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
...req.query,
projectId: req.query.workspaceId
});
return { approvals };
return { approvals, totalCount };
}
});
@@ -725,16 +725,17 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
)
.where(`${TableName.Environment}.projectId`, projectId)
.where(`${TableName.AccessApprovalPolicy}.deletedAt`, null)
.select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"));
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"))
.select(db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"));
const formattedRequests = sqlNestRelationships({
data: accessRequests,
key: "id",
parentMapper: (doc) => ({
...AccessApprovalRequestsSchema.parse(doc)
...AccessApprovalRequestsSchema.parse(doc),
isPolicyDeleted: Boolean(doc.policyDeletedAt)
}),
childrenMapper: [
{
@@ -751,7 +752,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
(req) =>
!req.privilegeId &&
!req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) &&
req.status === ApprovalStatus.PENDING
req.status === ApprovalStatus.PENDING &&
!req.isPolicyDeleted
);
// an approval is finalized if there are any rejections, a privilege ID is set or the number of approvals is equal to the number of approvals required.
@@ -759,7 +761,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
(req) =>
req.privilegeId ||
req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) ||
req.status !== ApprovalStatus.PENDING
req.status !== ApprovalStatus.PENDING ||
req.isPolicyDeleted
);
return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length };
@@ -275,7 +275,7 @@ export const accessApprovalRequestServiceFactory = ({
const listApprovalRequests: TAccessApprovalRequestServiceFactory["listApprovalRequests"] = async ({
projectSlug,
authorProjectMembershipId,
authorUserId,
envSlug,
actor,
actorOrgId,
@@ -300,8 +300,8 @@ export const accessApprovalRequestServiceFactory = ({
const policies = await accessApprovalPolicyDAL.find({ projectId: project.id });
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
if (authorProjectMembershipId) {
requests = requests.filter((request) => request.requestedByUserId === actorId);
if (authorUserId) {
requests = requests.filter((request) => request.requestedByUserId === authorUserId);
}
if (envSlug) {
@@ -31,7 +31,7 @@ export type TCreateAccessApprovalRequestDTO = {
export type TListApprovalRequestsDTO = {
projectSlug: string;
authorProjectMembershipId?: string;
authorUserId?: string;
envSlug?: string;
} & Omit<TProjectPermission, "projectId">;
@@ -3,9 +3,43 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { DynamicSecretLeasesSchema, TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { ormify, selectAllTableCols, TOrmify } from "@app/lib/knex";
export type TDynamicSecretLeaseDALFactory = ReturnType<typeof dynamicSecretLeaseDALFactory>;
export interface TDynamicSecretLeaseDALFactory extends Omit<TOrmify<TableName.DynamicSecretLease>, "findById"> {
countLeasesForDynamicSecret: (dynamicSecretId: string, tx?: Knex) => Promise<number>;
findById: (
id: string,
tx?: Knex
) => Promise<
| {
dynamicSecret: {
id: string;
name: string;
version: number;
type: string;
defaultTTL: string;
maxTTL: string | null | undefined;
encryptedInput: Buffer;
folderId: string;
status: string | null | undefined;
statusDetails: string | null | undefined;
createdAt: Date;
updatedAt: Date;
};
version: number;
id: string;
createdAt: Date;
updatedAt: Date;
externalEntityId: string;
expireAt: Date;
dynamicSecretId: string;
status?: string | null | undefined;
config?: unknown;
statusDetails?: string | null | undefined;
}
| undefined
>;
}
export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.DynamicSecretLease);
@@ -21,7 +21,12 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
};
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
export type TDynamicSecretLeaseQueueServiceFactory = {
pruneDynamicSecret: (dynamicSecretCfgId: string) => Promise<void>;
setLeaseRevocation: (leaseId: string, expiryAt: Date) => Promise<void>;
unsetLeaseRevocation: (leaseId: string) => Promise<void>;
init: () => Promise<void>;
};
export const dynamicSecretLeaseQueueServiceFactory = ({
queueService,
@@ -30,55 +35,48 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
dynamicSecretLeaseDAL,
kmsService,
folderDAL
}: TDynamicSecretLeaseQueueServiceFactoryDep) => {
}: TDynamicSecretLeaseQueueServiceFactoryDep): TDynamicSecretLeaseQueueServiceFactory => {
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
await queueService.queue(
QueueName.DynamicSecretRevocation,
await queueService.queuePg<QueueName.DynamicSecretRevocation>(
QueueJobs.DynamicSecretPruning,
{ dynamicSecretCfgId },
{
jobId: dynamicSecretCfgId,
backoff: {
type: "exponential",
delay: 3000
},
removeOnFail: {
count: 3
},
removeOnComplete: true
singletonKey: dynamicSecretCfgId,
retryLimit: 3,
retryBackoff: true
}
);
};
const setLeaseRevocation = async (leaseId: string, expiry: number) => {
await queueService.queue(
QueueName.DynamicSecretRevocation,
const setLeaseRevocation = async (leaseId: string, expiryAt: Date) => {
await queueService.queuePg<QueueName.DynamicSecretRevocation>(
QueueJobs.DynamicSecretRevocation,
{ leaseId },
{
jobId: leaseId,
backoff: {
type: "exponential",
delay: 3000
},
delay: expiry,
removeOnFail: {
count: 3
},
removeOnComplete: true
id: leaseId,
singletonKey: leaseId,
startAfter: expiryAt,
retryLimit: 3,
retryBackoff: true,
retentionDays: 2
}
);
};
const unsetLeaseRevocation = async (leaseId: string) => {
await queueService.stopJobById(QueueName.DynamicSecretRevocation, leaseId);
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, leaseId);
};
queueService.start(QueueName.DynamicSecretRevocation, async (job) => {
const $dynamicSecretQueueJob = async (
jobName: string,
jobId: string,
data: { leaseId: string } | { dynamicSecretCfgId: string }
): Promise<void> => {
try {
if (job.name === QueueJobs.DynamicSecretRevocation) {
const { leaseId } = job.data as { leaseId: string };
logger.info("Dynamic secret lease revocation started: ", leaseId, job.id);
if (jobName === QueueJobs.DynamicSecretRevocation) {
const { leaseId } = data as { leaseId: string };
logger.info("Dynamic secret lease revocation started: ", leaseId, jobId);
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
@@ -107,9 +105,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
return;
}
if (job.name === QueueJobs.DynamicSecretPruning) {
const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string };
logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, job.id);
if (jobName === QueueJobs.DynamicSecretPruning) {
const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string };
logger.info("Dynamic secret pruning started: ", dynamicSecretCfgId, jobId);
const dynamicSecretCfg = await dynamicSecretDAL.findById(dynamicSecretCfgId);
if (!dynamicSecretCfg) throw new DisableRotationErrors({ message: "Dynamic secret not found" });
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
@@ -150,38 +148,68 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
await dynamicSecretDAL.deleteById(dynamicSecretCfgId);
}
logger.info("Finished dynamic secret job", job.id);
logger.info("Finished dynamic secret job", jobId);
} catch (error) {
logger.error(error);
if (job?.name === QueueJobs.DynamicSecretPruning) {
const { dynamicSecretCfgId } = job.data as { dynamicSecretCfgId: string };
if (jobName === QueueJobs.DynamicSecretPruning) {
const { dynamicSecretCfgId } = data as { dynamicSecretCfgId: string };
await dynamicSecretDAL.updateById(dynamicSecretCfgId, {
status: DynamicSecretStatus.FailedDeletion,
statusDetails: (error as Error)?.message?.slice(0, 255)
});
}
if (job?.name === QueueJobs.DynamicSecretRevocation) {
const { leaseId } = job.data as { leaseId: string };
if (jobName === QueueJobs.DynamicSecretRevocation) {
const { leaseId } = data as { leaseId: string };
await dynamicSecretLeaseDAL.updateById(leaseId, {
status: DynamicSecretStatus.FailedDeletion,
statusDetails: (error as Error)?.message?.slice(0, 255)
});
}
if (error instanceof DisableRotationErrors) {
if (job.id) {
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, job.id);
if (jobId) {
await queueService.stopRepeatableJobByJobId(QueueName.DynamicSecretRevocation, jobId);
await queueService.stopJobByIdPg(QueueName.DynamicSecretRevocation, jobId);
}
}
// propogate to next part
throw error;
}
};
queueService.start(QueueName.DynamicSecretRevocation, async (job) => {
await $dynamicSecretQueueJob(job.name, job.id as string, job.data);
});
const init = async () => {
await queueService.startPg<QueueName.DynamicSecretRevocation>(
QueueJobs.DynamicSecretRevocation,
async ([job]) => {
await $dynamicSecretQueueJob(job.name, job.id, job.data);
},
{
workerCount: 5,
pollingIntervalSeconds: 1
}
);
await queueService.startPg<QueueName.DynamicSecretRevocation>(
QueueJobs.DynamicSecretPruning,
async ([job]) => {
await $dynamicSecretQueueJob(job.name, job.id, job.data);
},
{
workerCount: 1,
pollingIntervalSeconds: 1
}
);
};
return {
pruneDynamicSecret,
setLeaseRevocation,
unsetLeaseRevocation
unsetLeaseRevocation,
init
};
};
@@ -26,12 +26,8 @@ import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "./dynamic-secret-lease-queue";
import {
DynamicSecretLeaseStatus,
TCreateDynamicSecretLeaseDTO,
TDeleteDynamicSecretLeaseDTO,
TDetailsDynamicSecretLeaseDTO,
TDynamicSecretLeaseConfig,
TListDynamicSecretLeasesDTO,
TRenewDynamicSecretLeaseDTO
TDynamicSecretLeaseServiceFactory
} from "./dynamic-secret-lease-types";
type TDynamicSecretLeaseServiceFactoryDep = {
@@ -48,8 +44,6 @@ type TDynamicSecretLeaseServiceFactoryDep = {
identityDAL: TIdentityDALFactory;
};
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
export const dynamicSecretLeaseServiceFactory = ({
dynamicSecretLeaseDAL,
dynamicSecretProviders,
@@ -62,14 +56,14 @@ export const dynamicSecretLeaseServiceFactory = ({
kmsService,
userDAL,
identityDAL
}: TDynamicSecretLeaseServiceFactoryDep) => {
}: TDynamicSecretLeaseServiceFactoryDep): TDynamicSecretLeaseServiceFactory => {
const extractEmailUsername = (email: string) => {
const regex = new RE2(/^([^@]+)/);
const match = email.match(regex);
return match ? match[1] : email;
};
const create = async ({
const create: TDynamicSecretLeaseServiceFactory["create"] = async ({
environmentSlug,
path,
name,
@@ -80,7 +74,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorAuthMethod,
ttl,
config
}: TCreateDynamicSecretLeaseDTO) => {
}) => {
const appCfg = getConfig();
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -184,11 +178,11 @@ export const dynamicSecretLeaseServiceFactory = ({
config
});
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt);
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
};
const renewLease = async ({
const renewLease: TDynamicSecretLeaseServiceFactory["renewLease"] = async ({
ttl,
actorAuthMethod,
actorOrgId,
@@ -198,7 +192,7 @@ export const dynamicSecretLeaseServiceFactory = ({
path,
environmentSlug,
leaseId
}: TRenewDynamicSecretLeaseDTO) => {
}) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -278,7 +272,7 @@ export const dynamicSecretLeaseServiceFactory = ({
);
await dynamicSecretQueueService.unsetLeaseRevocation(dynamicSecretLease.id);
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, expireAt);
const updatedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
expireAt,
externalEntityId: entityId
@@ -286,7 +280,7 @@ export const dynamicSecretLeaseServiceFactory = ({
return updatedDynamicSecretLease;
};
const revokeLease = async ({
const revokeLease: TDynamicSecretLeaseServiceFactory["revokeLease"] = async ({
leaseId,
environmentSlug,
path,
@@ -296,7 +290,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId,
actorAuthMethod,
isForced
}: TDeleteDynamicSecretLeaseDTO) => {
}) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -376,7 +370,7 @@ export const dynamicSecretLeaseServiceFactory = ({
return deletedDynamicSecretLease;
};
const listLeases = async ({
const listLeases: TDynamicSecretLeaseServiceFactory["listLeases"] = async ({
path,
name,
actor,
@@ -385,7 +379,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorOrgId,
environmentSlug,
actorAuthMethod
}: TListDynamicSecretLeasesDTO) => {
}) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -424,7 +418,7 @@ export const dynamicSecretLeaseServiceFactory = ({
return dynamicSecretLeases;
};
const getLeaseDetails = async ({
const getLeaseDetails: TDynamicSecretLeaseServiceFactory["getLeaseDetails"] = async ({
projectSlug,
actorOrgId,
path,
@@ -433,7 +427,7 @@ export const dynamicSecretLeaseServiceFactory = ({
actorId,
leaseId,
actorAuthMethod
}: TDetailsDynamicSecretLeaseDTO) => {
}) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -1,4 +1,5 @@
import { TProjectPermission } from "@app/lib/types";
import { TDynamicSecretLeases } from "@app/db/schemas";
import { TDynamicSecretWithMetadata, TProjectPermission } from "@app/lib/types";
export enum DynamicSecretLeaseStatus {
FailedDeletion = "Failed to delete"
@@ -48,3 +49,40 @@ export type TDynamicSecretKubernetesLeaseConfig = {
};
export type TDynamicSecretLeaseConfig = TDynamicSecretKubernetesLeaseConfig;
export type TDynamicSecretLeaseServiceFactory = {
create: (arg: TCreateDynamicSecretLeaseDTO) => Promise<{
lease: TDynamicSecretLeases;
dynamicSecret: TDynamicSecretWithMetadata;
data: unknown;
}>;
listLeases: (arg: TListDynamicSecretLeasesDTO) => Promise<TDynamicSecretLeases[]>;
revokeLease: (arg: TDeleteDynamicSecretLeaseDTO) => Promise<TDynamicSecretLeases>;
renewLease: (arg: TRenewDynamicSecretLeaseDTO) => Promise<TDynamicSecretLeases>;
getLeaseDetails: (arg: TDetailsDynamicSecretLeaseDTO) => Promise<{
dynamicSecret: {
id: string;
name: string;
version: number;
type: string;
defaultTTL: string;
maxTTL: string | null | undefined;
encryptedInput: Buffer;
folderId: string;
status: string | null | undefined;
statusDetails: string | null | undefined;
createdAt: Date;
updatedAt: Date;
};
version: number;
id: string;
createdAt: Date;
updatedAt: Date;
externalEntityId: string;
expireAt: Date;
dynamicSecretId: string;
status?: string | null | undefined;
config?: unknown;
statusDetails?: string | null | undefined;
}>;
};
@@ -10,17 +10,35 @@ import {
selectAllTableCols,
sqlNestRelationships,
TFindFilter,
TFindOpt
TFindOpt,
TOrmify
} from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { OrderByDirection, TDynamicSecretWithMetadata } from "@app/lib/types";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory>;
export interface TDynamicSecretDALFactory extends Omit<TOrmify<TableName.DynamicSecret>, "findOne"> {
findOne: (filter: TFindFilter<TDynamicSecrets>, tx?: Knex) => Promise<TDynamicSecretWithMetadata>;
listDynamicSecretsByFolderIds: (
arg: {
folderIds: string[];
search?: string | undefined;
limit?: number | undefined;
offset?: number | undefined;
orderBy?: SecretsOrderBy | undefined;
orderDirection?: OrderByDirection | undefined;
},
tx?: Knex
) => Promise<Array<TDynamicSecretWithMetadata & { environment: string }>>;
findWithMetadata: (
filter: TFindFilter<TDynamicSecrets>,
arg?: TFindOpt<TDynamicSecrets>
) => Promise<TDynamicSecretWithMetadata[]>;
}
export const dynamicSecretDALFactory = (db: TDbClient) => {
export const dynamicSecretDALFactory = (db: TDbClient): TDynamicSecretDALFactory => {
const orm = ormify(db, TableName.DynamicSecret);
const findOne = async (filter: TFindFilter<TDynamicSecrets>, tx?: Knex) => {
const findOne: TDynamicSecretDALFactory["findOne"] = async (filter, tx) => {
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
.leftJoin(
TableName.ResourceMetadata,
@@ -55,9 +73,9 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
return docs[0];
};
const findWithMetadata = async (
filter: TFindFilter<TDynamicSecrets>,
{ offset, limit, sort, tx }: TFindOpt<TDynamicSecrets> = {}
const findWithMetadata: TDynamicSecretDALFactory["findWithMetadata"] = async (
filter,
{ offset, limit, sort, tx } = {}
) => {
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
.leftJoin(
@@ -101,23 +119,9 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
};
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
const listDynamicSecretsByFolderIds = async (
{
folderIds,
search,
limit,
offset = 0,
orderBy = SecretsOrderBy.Name,
orderDirection = OrderByDirection.ASC
}: {
folderIds: string[];
search?: string;
limit?: number;
offset?: number;
orderBy?: SecretsOrderBy;
orderDirection?: OrderByDirection;
},
tx?: Knex
const listDynamicSecretsByFolderIds: TDynamicSecretDALFactory["listDynamicSecretsByFolderIds"] = async (
{ folderIds, search, limit, offset = 0, orderBy = SecretsOrderBy.Name, orderDirection = OrderByDirection.ASC },
tx
) => {
try {
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
@@ -8,7 +8,7 @@ import {
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { OrderByDirection } from "@app/lib/types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -20,17 +20,7 @@ import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/
import { TGatewayDALFactory } from "../gateway/gateway-dal";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import {
DynamicSecretStatus,
TCreateDynamicSecretDTO,
TDeleteDynamicSecretDTO,
TDetailsDynamicSecretDTO,
TGetDynamicSecretsCountDTO,
TListDynamicSecretsByFolderMappingsDTO,
TListDynamicSecretsDTO,
TListDynamicSecretsMultiEnvDTO,
TUpdateDynamicSecretDTO
} from "./dynamic-secret-types";
import { DynamicSecretStatus, TDynamicSecretServiceFactory } from "./dynamic-secret-types";
import { AzureEntraIDProvider } from "./providers/azure-entra-id";
import { DynamicSecretProviders, TDynamicProviderFns } from "./providers/models";
@@ -51,8 +41,6 @@ type TDynamicSecretServiceFactoryDep = {
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
export const dynamicSecretServiceFactory = ({
dynamicSecretDAL,
dynamicSecretLeaseDAL,
@@ -65,8 +53,8 @@ export const dynamicSecretServiceFactory = ({
kmsService,
gatewayDAL,
resourceMetadataDAL
}: TDynamicSecretServiceFactoryDep) => {
const create = async ({
}: TDynamicSecretServiceFactoryDep): TDynamicSecretServiceFactory => {
const create: TDynamicSecretServiceFactory["create"] = async ({
path,
actor,
name,
@@ -80,7 +68,7 @@ export const dynamicSecretServiceFactory = ({
actorAuthMethod,
metadata,
usernameTemplate
}: TCreateDynamicSecretDTO) => {
}) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -188,7 +176,7 @@ export const dynamicSecretServiceFactory = ({
return dynamicSecretCfg;
};
const updateByName = async ({
const updateByName: TDynamicSecretServiceFactory["updateByName"] = async ({
name,
maxTTL,
defaultTTL,
@@ -203,7 +191,7 @@ export const dynamicSecretServiceFactory = ({
actorAuthMethod,
metadata,
usernameTemplate
}: TUpdateDynamicSecretDTO) => {
}) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -345,7 +333,7 @@ export const dynamicSecretServiceFactory = ({
return updatedDynamicCfg;
};
const deleteByName = async ({
const deleteByName: TDynamicSecretServiceFactory["deleteByName"] = async ({
actorAuthMethod,
actorOrgId,
actorId,
@@ -355,7 +343,7 @@ export const dynamicSecretServiceFactory = ({
path,
environmentSlug,
isForced
}: TDeleteDynamicSecretDTO) => {
}) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -413,7 +401,7 @@ export const dynamicSecretServiceFactory = ({
return deletedDynamicSecretCfg;
};
const getDetails = async ({
const getDetails: TDynamicSecretServiceFactory["getDetails"] = async ({
name,
projectSlug,
path,
@@ -422,7 +410,7 @@ export const dynamicSecretServiceFactory = ({
actorOrgId,
actorId,
actor
}: TDetailsDynamicSecretDTO) => {
}) => {
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -480,7 +468,7 @@ export const dynamicSecretServiceFactory = ({
};
// get unique dynamic secret count across multiple envs
const getCountMultiEnv = async ({
const getCountMultiEnv: TDynamicSecretServiceFactory["getCountMultiEnv"] = async ({
actorAuthMethod,
actorOrgId,
actorId,
@@ -490,7 +478,7 @@ export const dynamicSecretServiceFactory = ({
environmentSlugs,
search,
isInternal
}: TListDynamicSecretsMultiEnvDTO) => {
}) => {
if (!isInternal) {
const { permission } = await permissionService.getProjectPermission({
actor,
@@ -526,7 +514,7 @@ export const dynamicSecretServiceFactory = ({
};
// get dynamic secret count for a single env
const getDynamicSecretCount = async ({
const getDynamicSecretCount: TDynamicSecretServiceFactory["getDynamicSecretCount"] = async ({
actorAuthMethod,
actorOrgId,
actorId,
@@ -535,7 +523,7 @@ export const dynamicSecretServiceFactory = ({
environmentSlug,
search,
projectId
}: TGetDynamicSecretsCountDTO) => {
}) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
@@ -561,7 +549,7 @@ export const dynamicSecretServiceFactory = ({
return Number(dynamicSecretCfg[0]?.count ?? 0);
};
const listDynamicSecretsByEnv = async ({
const listDynamicSecretsByEnv: TDynamicSecretServiceFactory["listDynamicSecretsByEnv"] = async ({
actorAuthMethod,
actorOrgId,
actorId,
@@ -575,7 +563,7 @@ export const dynamicSecretServiceFactory = ({
orderDirection = OrderByDirection.ASC,
search,
...params
}: TListDynamicSecretsDTO) => {
}) => {
let { projectId } = params;
if (!projectId) {
@@ -619,9 +607,9 @@ export const dynamicSecretServiceFactory = ({
});
};
const listDynamicSecretsByFolderIds = async (
{ folderMappings, filters, projectId }: TListDynamicSecretsByFolderMappingsDTO,
actor: OrgServiceActor
const listDynamicSecretsByFolderIds: TDynamicSecretServiceFactory["listDynamicSecretsByFolderIds"] = async (
{ folderMappings, filters, projectId },
actor
) => {
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
@@ -657,7 +645,7 @@ export const dynamicSecretServiceFactory = ({
};
// get dynamic secrets for multiple envs
const listDynamicSecretsByEnvs = async ({
const listDynamicSecretsByEnvs: TDynamicSecretServiceFactory["listDynamicSecretsByEnvs"] = async ({
actorAuthMethod,
actorOrgId,
actorId,
@@ -667,7 +655,7 @@ export const dynamicSecretServiceFactory = ({
projectId,
isInternal,
...params
}: TListDynamicSecretsMultiEnvDTO) => {
}) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
@@ -700,14 +688,10 @@ export const dynamicSecretServiceFactory = ({
});
};
const fetchAzureEntraIdUsers = async ({
const fetchAzureEntraIdUsers: TDynamicSecretServiceFactory["fetchAzureEntraIdUsers"] = async ({
tenantId,
applicationId,
clientSecret
}: {
tenantId: string;
applicationId: string;
clientSecret: string;
}) => {
const azureEntraIdUsers = await AzureEntraIDProvider().fetchAzureEntraIdUsers(
tenantId,
@@ -1,6 +1,7 @@
import { z } from "zod";
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { TDynamicSecrets } from "@app/db/schemas";
import { OrderByDirection, OrgServiceActor, TDynamicSecretWithMetadata, TProjectPermission } from "@app/lib/types";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
@@ -83,3 +84,27 @@ export type TListDynamicSecretsMultiEnvDTO = Omit<
export type TGetDynamicSecretsCountDTO = Omit<TListDynamicSecretsDTO, "projectSlug" | "projectId"> & {
projectId: string;
};
export type TDynamicSecretServiceFactory = {
create: (arg: TCreateDynamicSecretDTO) => Promise<TDynamicSecrets>;
updateByName: (arg: TUpdateDynamicSecretDTO) => Promise<TDynamicSecrets>;
deleteByName: (arg: TDeleteDynamicSecretDTO) => Promise<TDynamicSecrets>;
getDetails: (arg: TDetailsDynamicSecretDTO) => Promise<TDynamicSecretWithMetadata>;
listDynamicSecretsByEnv: (arg: TListDynamicSecretsDTO) => Promise<TDynamicSecretWithMetadata[]>;
listDynamicSecretsByEnvs: (
arg: TListDynamicSecretsMultiEnvDTO
) => Promise<Array<TDynamicSecretWithMetadata & { environment: string }>>;
getDynamicSecretCount: (arg: TGetDynamicSecretsCountDTO) => Promise<number>;
getCountMultiEnv: (arg: TListDynamicSecretsMultiEnvDTO) => Promise<number>;
fetchAzureEntraIdUsers: (arg: { tenantId: string; applicationId: string; clientSecret: string }) => Promise<
{
name: string;
id: string;
email: string;
}[]
>;
listDynamicSecretsByFolderIds: (
arg: TListDynamicSecretsByFolderMappingsDTO,
actor: OrgServiceActor
) => Promise<Array<TDynamicSecretWithMetadata & { environment: string; path: string }>>;
};
@@ -128,11 +128,21 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
const username = generateUsername(usernameTemplate, identity);
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }];
if (providerInputs.tags && Array.isArray(providerInputs.tags)) {
const additionalTags = providerInputs.tags.map((tag) => ({
Key: tag.key,
Value: tag.value
}));
awsTags.push(...additionalTags);
}
const createUserRes = await client.send(
new CreateUserCommand({
Path: awsPath,
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
Tags: [{ Key: "createdBy", Value: "infisical-dynamic-secret" }],
Tags: awsTags,
UserName: username
})
);
@@ -0,0 +1,133 @@
import axios from "axios";
import * as jwt from "jsonwebtoken";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { DynamicSecretGithubSchema, TDynamicProviderFns } from "./models";
interface GitHubInstallationTokenResponse {
token: string;
expires_at: string; // ISO 8601 timestamp e.g., "2024-01-15T12:00:00Z"
permissions?: Record<string, string>;
repository_selection?: string;
}
interface TGithubProviderInputs {
appId: number;
installationId: number;
privateKey: string;
}
export const GithubProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretGithubSchema.parseAsync(inputs);
return providerInputs;
};
const $generateGitHubInstallationAccessToken = async (
credentials: TGithubProviderInputs
): Promise<GitHubInstallationTokenResponse> => {
const { appId, installationId, privateKey } = credentials;
const nowInSeconds = Math.floor(Date.now() / 1000);
const jwtPayload = {
iat: nowInSeconds - 5,
exp: nowInSeconds + 60,
iss: String(appId)
};
let appJwt: string;
try {
appJwt = jwt.sign(jwtPayload, privateKey, { algorithm: "RS256" });
} catch (error) {
let message = "Failed to sign JWT.";
if (error instanceof jwt.JsonWebTokenError) {
message += ` JsonWebTokenError: ${error.message}`;
}
throw new InternalServerError({
message
});
}
const tokenUrl = `${IntegrationUrls.GITHUB_API_URL}/app/installations/${String(installationId)}/access_tokens`;
try {
const response = await axios.post<GitHubInstallationTokenResponse>(tokenUrl, undefined, {
headers: {
Authorization: `Bearer ${appJwt}`,
Accept: "application/vnd.github.v3+json",
"X-GitHub-Api-Version": "2022-11-28"
}
});
if (response.status === 201 && response.data.token) {
return response.data; // Includes token, expires_at, permissions, repository_selection
}
throw new InternalServerError({
message: `GitHub API responded with unexpected status ${response.status}: ${JSON.stringify(response.data)}`
});
} catch (error) {
let message = "Failed to fetch GitHub installation access token.";
if (axios.isAxiosError(error) && error.response) {
const githubErrorMsg =
(error.response.data as { message?: string })?.message || JSON.stringify(error.response.data);
message += ` GitHub API Error: ${error.response.status} - ${githubErrorMsg}`;
// Classify as BadRequestError for auth-related issues (401, 403, 404) which might be due to user input
if ([401, 403, 404].includes(error.response.status)) {
throw new BadRequestError({ message });
}
}
throw new InternalServerError({ message });
}
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
await $generateGitHubInstallationAccessToken(providerInputs);
return true;
};
const create = async (data: { inputs: unknown }) => {
const { inputs } = data;
const providerInputs = await validateProviderInputs(inputs);
const ghTokenData = await $generateGitHubInstallationAccessToken(providerInputs);
const entityId = alphaNumericNanoId(32);
return {
entityId,
data: {
TOKEN: ghTokenData.token,
EXPIRES_AT: ghTokenData.expires_at,
PERMISSIONS: ghTokenData.permissions,
REPOSITORY_SELECTION: ghTokenData.repository_selection
}
};
};
const revoke = async () => {
// GitHub installation tokens cannot be revoked.
throw new BadRequestError({
message:
"Github dynamic secret does not support revocation because GitHub itself cannot revoke installation tokens"
});
};
const renew = async () => {
// No renewal
throw new BadRequestError({ message: "Github dynamic secret does not support renewal" });
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};
@@ -7,6 +7,7 @@ import { AzureEntraIDProvider } from "./azure-entra-id";
import { CassandraProvider } from "./cassandra";
import { ElasticSearchProvider } from "./elastic-search";
import { GcpIamProvider } from "./gcp-iam";
import { GithubProvider } from "./github";
import { KubernetesProvider } from "./kubernetes";
import { LdapProvider } from "./ldap";
import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
@@ -44,5 +45,6 @@ export const buildDynamicSecretProviders = ({
[DynamicSecretProviders.SapAse]: SapAseProvider(),
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }),
[DynamicSecretProviders.GcpIam]: GcpIamProvider()
[DynamicSecretProviders.GcpIam]: GcpIamProvider(),
[DynamicSecretProviders.Github]: GithubProvider()
});
@@ -52,9 +52,8 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: string;
targetHost: string;
targetPort: number;
caCert?: string;
httpsAgent?: https.Agent;
reviewTokenThroughGateway: boolean;
enableSsl: boolean;
},
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
): Promise<T> => {
@@ -85,10 +84,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
key: relayDetails.privateKey.toString()
},
// we always pass this, because its needed for both tcp and http protocol
httpsAgent: new https.Agent({
ca: inputs.caCert,
rejectUnauthorized: inputs.enableSsl
})
httpsAgent: inputs.httpsAgent
}
);
@@ -311,6 +307,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
const k8sHost = `${url.protocol}//${url.hostname}`;
try {
const httpsAgent =
providerInputs.ca && providerInputs.sslEnabled
? new https.Agent({
ca: providerInputs.ca,
rejectUnauthorized: true
})
: undefined;
if (providerInputs.gatewayId) {
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
await $gatewayProxyWrapper(
@@ -318,8 +322,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId,
targetHost: k8sHost,
targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled,
caCert: providerInputs.ca,
httpsAgent,
reviewTokenThroughGateway: true
},
providerInputs.credentialType === KubernetesCredentialType.Static
@@ -332,8 +335,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId,
targetHost: k8sGatewayHost,
targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled,
caCert: providerInputs.ca,
httpsAgent,
reviewTokenThroughGateway: false
},
providerInputs.credentialType === KubernetesCredentialType.Static
@@ -342,9 +344,9 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
);
}
} else if (providerInputs.credentialType === KubernetesCredentialType.Static) {
await serviceAccountStaticCallback(k8sHost, k8sPort);
await serviceAccountStaticCallback(k8sHost, k8sPort, httpsAgent);
} else {
await serviceAccountDynamicCallback(k8sHost, k8sPort);
await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
}
return true;
@@ -546,6 +548,15 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
try {
let tokenData;
const httpsAgent =
providerInputs.ca && providerInputs.sslEnabled
? new https.Agent({
ca: providerInputs.ca,
rejectUnauthorized: true
})
: undefined;
if (providerInputs.gatewayId) {
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
tokenData = await $gatewayProxyWrapper(
@@ -553,8 +564,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId,
targetHost: k8sHost,
targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled,
caCert: providerInputs.ca,
httpsAgent,
reviewTokenThroughGateway: true
},
providerInputs.credentialType === KubernetesCredentialType.Static
@@ -567,8 +577,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId,
targetHost: k8sGatewayHost,
targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled,
caCert: providerInputs.ca,
httpsAgent,
reviewTokenThroughGateway: false
},
providerInputs.credentialType === KubernetesCredentialType.Static
@@ -579,8 +588,8 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
} else {
tokenData =
providerInputs.credentialType === KubernetesCredentialType.Static
? await tokenRequestStaticCallback(k8sHost, k8sPort)
: await serviceAccountDynamicCallback(k8sHost, k8sPort);
? await tokenRequestStaticCallback(k8sHost, k8sPort, httpsAgent)
: await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
}
return {
@@ -684,6 +693,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
const k8sPort = url.port ? Number(url.port) : 443;
const k8sHost = `${url.protocol}//${url.hostname}`;
const httpsAgent =
providerInputs.ca && providerInputs.sslEnabled
? new https.Agent({
ca: providerInputs.ca,
rejectUnauthorized: true
})
: undefined;
if (providerInputs.gatewayId) {
if (providerInputs.authMethod === KubernetesAuthMethod.Gateway) {
await $gatewayProxyWrapper(
@@ -691,8 +708,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId,
targetHost: k8sHost,
targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled,
caCert: providerInputs.ca,
httpsAgent,
reviewTokenThroughGateway: true
},
serviceAccountDynamicCallback
@@ -703,15 +719,14 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
gatewayId: providerInputs.gatewayId,
targetHost: k8sGatewayHost,
targetPort: k8sPort,
enableSsl: providerInputs.sslEnabled,
caCert: providerInputs.ca,
httpsAgent,
reviewTokenThroughGateway: false
},
serviceAccountDynamicCallback
);
}
} else {
await serviceAccountDynamicCallback(k8sHost, k8sPort);
await serviceAccountDynamicCallback(k8sHost, k8sPort, httpsAgent);
}
}
@@ -2,6 +2,7 @@ import RE2 from "re2";
import { z } from "zod";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { TDynamicSecretLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types";
@@ -207,7 +208,8 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
permissionBoundaryPolicyArn: z.string().trim().optional(),
policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(),
policyArns: z.string().trim().optional()
policyArns: z.string().trim().optional(),
tags: ResourceMetadataSchema.optional()
}),
z.object({
method: z.literal(AwsIamAuthType.AssumeRole),
@@ -217,7 +219,8 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
permissionBoundaryPolicyArn: z.string().trim().optional(),
policyDocument: z.string().trim().optional(),
userGroups: z.string().trim().optional(),
policyArns: z.string().trim().optional()
policyArns: z.string().trim().optional(),
tags: ResourceMetadataSchema.optional()
})
])
);
@@ -474,6 +477,23 @@ export const DynamicSecretGcpIamSchema = z.object({
serviceAccountEmail: z.string().email().trim().min(1, "Service account email required").max(128)
});
export const DynamicSecretGithubSchema = z.object({
appId: z.number().min(1).describe("The ID of your GitHub App."),
installationId: z.number().min(1).describe("The ID of the GitHub App installation."),
privateKey: z
.string()
.trim()
.min(1)
.refine(
(val) =>
new RE2(
/^-----BEGIN(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----\s*[\s\S]*?-----END(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----$/
).test(val),
"Invalid PEM format for private key"
)
.describe("The private key generated for your GitHub App.")
});
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",
Cassandra = "cassandra",
@@ -492,7 +512,8 @@ export enum DynamicSecretProviders {
SapAse = "sap-ase",
Kubernetes = "kubernetes",
Vertica = "vertica",
GcpIam = "gcp-iam"
GcpIam = "gcp-iam",
Github = "github"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@@ -513,7 +534,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }),
z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema })
z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Github), inputs: DynamicSecretGithubSchema })
]);
export type TDynamicProviderFns = {
@@ -24,6 +24,7 @@ type TFindQueryFilter = {
committer?: string;
limit?: number;
offset?: number;
search?: string;
};
export const secretApprovalRequestDALFactory = (db: TDbClient) => {
@@ -314,7 +315,6 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
)
.andWhere((bd) => void bd.where(`${TableName.SecretApprovalPolicy}.deletedAt`, null))
.select("status", `${TableName.SecretApprovalRequest}.id`)
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
.count("status")
@@ -340,13 +340,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
};
const findByProjectId = async (
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
{ status, limit = 20, offset = 0, projectId, committer, environment, userId, search }: TFindQueryFilter,
tx?: Knex
) => {
try {
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
// this is the place u wanna look at.
const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
const innerQuery = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join(
@@ -435,7 +435,30 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
)
.orderBy("createdAt", "desc");
.distinctOn(`${TableName.SecretApprovalRequest}.id`)
.as("inner");
const query = (tx || db)
.select("*")
.select(db.raw("count(*) OVER() as total_count"))
.from(innerQuery)
.orderBy("createdAt", "desc") as typeof innerQuery;
if (search) {
void query.where((qb) => {
void qb
.whereRaw(`CONCAT_WS(' ', ??, ??) ilike ?`, [
db.ref("firstName").withSchema("committerUser"),
db.ref("lastName").withSchema("committerUser"),
`%${search}%`
])
.orWhereRaw(`?? ilike ?`, [db.ref("username").withSchema("committerUser"), `%${search}%`])
.orWhereRaw(`?? ilike ?`, [db.ref("email").withSchema("committerUser"), `%${search}%`])
.orWhereILike(`${TableName.Environment}.name`, `%${search}%`)
.orWhereILike(`${TableName.Environment}.slug`, `%${search}%`)
.orWhereILike(`${TableName.SecretApprovalPolicy}.secretPath`, `%${search}%`);
});
}
const docs = await (tx || db)
.with("w", query)
@@ -443,6 +466,10 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", offset)
.andWhere("w.rank", "<", offset + limit);
// @ts-expect-error knex does not infer
const totalCount = Number(docs[0]?.total_count || 0);
const formattedDoc = sqlNestRelationships({
data: docs,
key: "id",
@@ -504,23 +531,26 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
}
]
});
return formattedDoc.map((el) => ({
...el,
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
}));
return {
approvals: formattedDoc.map((el) => ({
...el,
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
})),
totalCount
};
} catch (error) {
throw new DatabaseError({ error, name: "FindSAR" });
}
};
const findByProjectIdBridgeSecretV2 = async (
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
{ status, limit = 20, offset = 0, projectId, committer, environment, userId, search }: TFindQueryFilter,
tx?: Knex
) => {
try {
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
// this is the place u wanna look at.
const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
const innerQuery = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join(
@@ -609,14 +639,42 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
)
.orderBy("createdAt", "desc");
.distinctOn(`${TableName.SecretApprovalRequest}.id`)
.as("inner");
const query = (tx || db)
.select("*")
.select(db.raw("count(*) OVER() as total_count"))
.from(innerQuery)
.orderBy("createdAt", "desc") as typeof innerQuery;
if (search) {
void query.where((qb) => {
void qb
.whereRaw(`CONCAT_WS(' ', ??, ??) ilike ?`, [
db.ref("firstName").withSchema("committerUser"),
db.ref("lastName").withSchema("committerUser"),
`%${search}%`
])
.orWhereRaw(`?? ilike ?`, [db.ref("username").withSchema("committerUser"), `%${search}%`])
.orWhereRaw(`?? ilike ?`, [db.ref("email").withSchema("committerUser"), `%${search}%`])
.orWhereILike(`${TableName.Environment}.name`, `%${search}%`)
.orWhereILike(`${TableName.Environment}.slug`, `%${search}%`)
.orWhereILike(`${TableName.SecretApprovalPolicy}.secretPath`, `%${search}%`);
});
}
const rankOffset = offset + 1;
const docs = await (tx || db)
.with("w", query)
.select("*")
.from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", offset)
.andWhere("w.rank", "<", offset + limit);
.where("w.rank", ">=", rankOffset)
.andWhere("w.rank", "<", rankOffset + limit);
// @ts-expect-error knex does not infer
const totalCount = Number(docs[0]?.total_count || 0);
const formattedDoc = sqlNestRelationships({
data: docs,
key: "id",
@@ -682,10 +740,13 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
}
]
});
return formattedDoc.map((el) => ({
...el,
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
}));
return {
approvals: formattedDoc.map((el) => ({
...el,
policy: { ...el.policy, approvers: el.approvers, bypassers: el.bypassers }
})),
totalCount
};
} catch (error) {
throw new DatabaseError({ error, name: "FindSAR" });
}
@@ -194,7 +194,8 @@ export const secretApprovalRequestServiceFactory = ({
environment,
committer,
limit,
offset
offset,
search
}: TListApprovalsDTO) => {
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
@@ -208,6 +209,7 @@ export const secretApprovalRequestServiceFactory = ({
});
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
return secretApprovalRequestDAL.findByProjectIdBridgeSecretV2({
projectId,
@@ -216,19 +218,21 @@ export const secretApprovalRequestServiceFactory = ({
status,
userId: actorId,
limit,
offset
offset,
search
});
}
const approvals = await secretApprovalRequestDAL.findByProjectId({
return secretApprovalRequestDAL.findByProjectId({
projectId,
committer,
environment,
status,
userId: actorId,
limit,
offset
offset,
search
});
return approvals;
};
const getSecretApprovalDetails = async ({
@@ -93,6 +93,7 @@ export type TListApprovalsDTO = {
committer?: string;
limit?: number;
offset?: number;
search?: string;
} & TProjectPermission;
export type TSecretApprovalDetailsDTO = {
+2 -1
View File
@@ -11,7 +11,8 @@ export const PgSqlLock = {
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`),
SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`),
CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`)
CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`),
CreateFolder: (envId: string, projectId: string) => pgAdvisoryLockHashText(`create-folder:${envId}-${projectId}`)
} as const;
// all the key prefixes used must be set here to avoid conflict
+4
View File
@@ -2418,6 +2418,10 @@ export const SecretSyncs = {
shouldProtectSecrets: "Whether variables should be protected",
shouldMaskSecrets: "Whether variables should be masked in logs",
shouldHideSecrets: "Whether variables should be hidden"
},
CLOUDFLARE_PAGES: {
projectName: "The name of the Cloudflare Pages project to sync secrets to.",
environment: "The environment of the Cloudflare Pages project to sync secrets to."
}
}
};
+2 -2
View File
@@ -101,9 +101,9 @@ const envSchema = z
LOOPS_API_KEY: zpStr(z.string().optional()),
// jwt options
AUTH_SECRET: zpStr(z.string()).default(process.env.JWT_AUTH_SECRET), // for those still using old JWT_AUTH_SECRET
JWT_AUTH_LIFETIME: zpStr(z.string().default("1d")),
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
JWT_REFRESH_LIFETIME: zpStr(z.string().default("14d")),
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
JWT_INVITE_LIFETIME: zpStr(z.string().default("1d")),
JWT_MFA_LIFETIME: zpStr(z.string().default("5m")),
JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")),
+2
View File
@@ -19,3 +19,5 @@ export const getMinExpiresIn = (exp1: string | number, exp2: string | number): s
return ms1 <= ms2 ? exp1 : exp2;
};
export const convertMsToSecond = (time: number) => time / 1000;
+5
View File
@@ -1,3 +1,4 @@
import { TDynamicSecrets } from "@app/db/schemas";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
export type TGenericPermission = {
@@ -84,3 +85,7 @@ export enum QueueWorkerProfile {
Standard = "standard",
SecretScanning = "secret-scanning"
}
export interface TDynamicSecretWithMetadata extends TDynamicSecrets {
metadata: { id: string; key: string; value: string }[];
}
+6
View File
@@ -377,6 +377,7 @@ export type TQueueServiceFactory = {
stopRepeatableJobByKey: <T extends QueueName>(name: T, repeatJobKey: string) => Promise<boolean>;
clearQueue: (name: QueueName) => Promise<void>;
stopJobById: <T extends QueueName>(name: T, jobId: string) => Promise<void | undefined>;
stopJobByIdPg: <T extends QueueName>(name: T, jobId: string) => Promise<void | undefined>;
getRepeatableJobs: (
name: QueueName,
startOffset?: number,
@@ -542,6 +543,10 @@ export const queueServiceFactory = (
return q.removeRepeatableByKey(repeatJobKey);
};
const stopJobByIdPg: TQueueServiceFactory["stopJobByIdPg"] = async (name, jobId) => {
await pgBoss.deleteJob(name, jobId);
};
const stopJobById: TQueueServiceFactory["stopJobById"] = async (name, jobId) => {
const q = queueContainer[name];
const job = await q.getJob(jobId);
@@ -568,6 +573,7 @@ export const queueServiceFactory = (
stopRepeatableJobByKey,
clearQueue,
stopJobById,
stopJobByIdPg,
getRepeatableJobs,
startPg,
queuePg,
@@ -107,7 +107,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
server.addHook("onRequest", async (req) => {
const appCfg = getConfig();
if (req.url.includes(".well-known/est") || req.url.includes("/api/v3/auth/") || req.url === "/api/v1/auth/token") {
if (req.url.includes(".well-known/est") || req.url.includes("/api/v3/auth/")) {
return;
}
+7
View File
@@ -1903,6 +1903,7 @@ export const registerRoutes = async (
await pkiSubscriberQueue.startDailyAutoRenewalJob();
await kmsService.startService();
await microsoftTeamsService.start();
await dynamicSecretQueueService.init();
// inject all services
server.decorate<FastifyZodProvider["services"]>("services", {
@@ -2020,10 +2021,16 @@ export const registerRoutes = async (
if (licenseSyncJob) {
cronJobs.push(licenseSyncJob);
}
const microsoftTeamsSyncJob = await microsoftTeamsService.initializeBackgroundSync();
if (microsoftTeamsSyncJob) {
cronJobs.push(microsoftTeamsSyncJob);
}
const adminIntegrationsSyncJob = await superAdminService.initializeAdminIntegrationConfigSync();
if (adminIntegrationsSyncJob) {
cronJobs.push(adminIntegrationsSyncJob);
}
}
server.decorate<FastifyZodProvider["store"]>("store", {
+18 -1
View File
@@ -37,7 +37,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
encryptedSlackClientSecret: true,
encryptedMicrosoftTeamsAppId: true,
encryptedMicrosoftTeamsClientSecret: true,
encryptedMicrosoftTeamsBotId: true
encryptedMicrosoftTeamsBotId: true,
encryptedGitHubAppConnectionClientId: true,
encryptedGitHubAppConnectionClientSecret: true,
encryptedGitHubAppConnectionSlug: true,
encryptedGitHubAppConnectionId: true,
encryptedGitHubAppConnectionPrivateKey: true
}).extend({
isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(),
@@ -87,6 +92,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
microsoftTeamsAppId: z.string().optional(),
microsoftTeamsClientSecret: z.string().optional(),
microsoftTeamsBotId: z.string().optional(),
gitHubAppConnectionClientId: z.string().optional(),
gitHubAppConnectionClientSecret: z.string().optional(),
gitHubAppConnectionSlug: z.string().optional(),
gitHubAppConnectionId: z.string().optional(),
gitHubAppConnectionPrivateKey: z.string().optional(),
authConsentContent: z
.string()
.trim()
@@ -348,6 +358,13 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
appId: z.string(),
clientSecret: z.string(),
botId: z.string()
}),
gitHubAppConnection: z.object({
clientId: z.string(),
clientSecret: z.string(),
appSlug: z.string(),
appId: z.string(),
privateKey: z.string()
})
})
}
@@ -35,6 +35,10 @@ import {
CamundaConnectionListItemSchema,
SanitizedCamundaConnectionSchema
} from "@app/services/app-connection/camunda";
import {
CloudflareConnectionListItemSchema,
SanitizedCloudflareConnectionSchema
} from "@app/services/app-connection/cloudflare/cloudflare-connection-schema";
import {
DatabricksConnectionListItemSchema,
SanitizedDatabricksConnectionSchema
@@ -111,7 +115,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedHerokuConnectionSchema.options,
...SanitizedRenderConnectionSchema.options,
...SanitizedFlyioConnectionSchema.options,
...SanitizedGitLabConnectionSchema.options
...SanitizedGitLabConnectionSchema.options,
...SanitizedCloudflareConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -142,7 +147,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
HerokuConnectionListItemSchema,
RenderConnectionListItemSchema,
FlyioConnectionListItemSchema,
GitLabConnectionListItemSchema
GitLabConnectionListItemSchema,
CloudflareConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -0,0 +1,53 @@
import z from "zod";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateCloudflareConnectionSchema,
SanitizedCloudflareConnectionSchema,
UpdateCloudflareConnectionSchema
} from "@app/services/app-connection/cloudflare/cloudflare-connection-schema";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerCloudflareConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Cloudflare,
server,
sanitizedResponseSchema: SanitizedCloudflareConnectionSchema,
createSchema: CreateCloudflareConnectionSchema,
updateSchema: UpdateCloudflareConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/cloudflare-pages-projects`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const projects = await server.services.appConnection.cloudflare.listPagesProjects(connectionId, req.permission);
return projects;
}
});
};
@@ -10,6 +10,7 @@ import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secre
import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router";
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
import { registerGcpConnectionRouter } from "./gcp-connection-router";
@@ -60,5 +61,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Heroku]: registerHerokuConnectionRouter,
[AppConnection.Render]: registerRenderConnectionRouter,
[AppConnection.Flyio]: registerFlyioConnectionRouter,
[AppConnection.GitLab]: registerGitLabConnectionRouter
[AppConnection.GitLab]: registerGitLabConnectionRouter,
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter
};
@@ -83,7 +83,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
config: {
rateLimit: smtpRateLimit({
keyGenerator: (req) =>
(req.body as { membershipId?: string })?.membershipId?.trim().substring(0, 100) ?? req.realIp
(req.body as { membershipId?: string })?.membershipId?.trim().substring(0, 100) || req.realIp
})
},
method: "POST",
@@ -81,7 +81,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
url: "/email/password-reset",
config: {
rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
})
},
schema: {
@@ -107,7 +107,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/email/password-reset-verify",
config: {
rateLimit: authRateLimit
rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
})
},
schema: {
body: z.object({
@@ -0,0 +1,16 @@
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
import {
CloudflarePagesSyncSchema,
CreateCloudflarePagesSyncSchema,
UpdateCloudflarePagesSyncSchema
} from "@app/services/secret-sync/cloudflare-pages/cloudflare-pages-schema";
export const registerCloudflarePagesSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.CloudflarePages,
server,
responseSchema: CloudflarePagesSyncSchema,
createSchema: CreateCloudflarePagesSyncSchema,
updateSchema: UpdateCloudflarePagesSyncSchema
});
@@ -8,6 +8,7 @@ import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configurati
import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router";
import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
import { registerCamundaSyncRouter } from "./camunda-sync-router";
import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router";
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
import { registerFlyioSyncRouter } from "./flyio-sync-router";
import { registerGcpSyncRouter } from "./gcp-sync-router";
@@ -45,5 +46,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Heroku]: registerHerokuSyncRouter,
[SecretSync.Render]: registerRenderSyncRouter,
[SecretSync.Flyio]: registerFlyioSyncRouter,
[SecretSync.GitLab]: registerGitLabSyncRouter
[SecretSync.GitLab]: registerGitLabSyncRouter,
[SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter
};
@@ -22,6 +22,10 @@ import {
import { AzureDevOpsSyncListItemSchema, AzureDevOpsSyncSchema } from "@app/services/secret-sync/azure-devops";
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
import {
CloudflarePagesSyncListItemSchema,
CloudflarePagesSyncSchema
} from "@app/services/secret-sync/cloudflare-pages/cloudflare-pages-schema";
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
import { FlyioSyncListItemSchema, FlyioSyncSchema } from "@app/services/secret-sync/flyio";
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
@@ -57,7 +61,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
HerokuSyncSchema,
RenderSyncSchema,
FlyioSyncSchema,
GitLabSyncSchema
GitLabSyncSchema,
CloudflarePagesSyncSchema
]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -81,7 +86,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
HerokuSyncListItemSchema,
RenderSyncListItemSchema,
FlyioSyncListItemSchema,
GitLabSyncListItemSchema
GitLabSyncListItemSchema,
CloudflarePagesSyncListItemSchema
]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
+5 -3
View File
@@ -2,7 +2,7 @@ import { z } from "zod";
import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
import { authRateLimit, readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
@@ -13,7 +13,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
url: "/me/emails/code",
config: {
rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) ?? req.realIp
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) || req.realIp
})
},
schema: {
@@ -34,7 +34,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/me/emails/verify",
config: {
rateLimit: authRateLimit
rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) || req.realIp
})
},
schema: {
body: z.object({
+4 -7
View File
@@ -50,8 +50,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
200: z.object({
token: z.string(),
isMfaEnabled: z.boolean(),
mfaMethod: z.string().optional(),
refreshToken: z.string().optional()
mfaMethod: z.string().optional()
})
}
},
@@ -102,7 +101,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
maxAge: 0
});
return { token: tokens.access, isMfaEnabled: false, refreshToken: tokens.refresh };
return { token: tokens.access, isMfaEnabled: false };
}
});
@@ -130,8 +129,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
encryptedPrivateKey: z.string(),
iv: z.string(),
tag: z.string(),
token: z.string(),
refreshToken: z.string().optional()
token: z.string()
})
}
},
@@ -174,8 +172,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
tag: data.user.tag,
protectedKey: data.user.protectedKey || null,
protectedKeyIV: data.user.protectedKeyIV || null,
protectedKeyTag: data.user.protectedKeyTag || null,
refreshToken: data.token.refresh
protectedKeyTag: data.user.protectedKeyTag || null
} as const;
}
});
+61 -26
View File
@@ -4,7 +4,7 @@ import { z } from "zod";
import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { secretsLimit, writeLimit } from "@app/server/config/rateLimiter";
import { BaseSecretNameSchema, SecretNameSchema } from "@app/server/lib/schemas";
@@ -12,7 +12,6 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { ProjectFilterType } from "@app/services/project/project-types";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
@@ -286,22 +285,17 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment = scope[0].environment;
workspaceId = req.auth.serviceToken.projectId;
}
} else if (req.permission.type === ActorType.IDENTITY && req.query.workspaceSlug && !workspaceId) {
const workspace = await server.services.project.getAProject({
filter: {
type: ProjectFilterType.SLUG,
orgId: req.permission.orgId,
slug: req.query.workspaceSlug
},
} else {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: req.query.workspaceSlug,
projectId: workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
if (!workspace) throw new NotFoundError({ message: `No project found with slug ${req.query.workspaceSlug}` });
workspaceId = workspace.id;
workspaceId = projectId;
}
if (!workspaceId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" });
@@ -442,11 +436,23 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment = scope[0].environment;
workspaceId = req.auth.serviceToken.projectId;
}
} else {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: workspaceSlug,
projectId: workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
workspaceId = projectId;
}
if (!environment) throw new BadRequestError({ message: "Missing environment" });
if (!workspaceId && !workspaceSlug)
if (!workspaceId) {
throw new BadRequestError({ message: "You must provide workspaceSlug or workspaceId" });
}
const secret = await server.services.secret.getSecretByNameRaw({
actorId: req.permission.id,
@@ -457,7 +463,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment,
projectId: workspaceId,
viewSecretValue: req.query.viewSecretValue,
projectSlug: workspaceSlug,
path: secretPath,
secretName: req.params.secretName,
type: req.query.type,
@@ -518,7 +523,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName)
}),
body: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.CREATE.workspaceId),
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.CREATE.workspaceId),
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.CREATE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment),
secretPath: z
.string()
@@ -558,13 +564,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: req.body.projectSlug,
projectId: req.body.workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
const secretOperation = await server.services.secret.createSecretRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environment: req.body.environment,
actorAuthMethod: req.permission.authMethod,
projectId: req.body.workspaceId,
projectId,
secretPath: req.body.secretPath,
secretName: req.params.secretName,
type: req.body.type,
@@ -582,7 +597,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({
projectId: req.body.workspaceId,
projectId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_SECRET,
@@ -602,7 +617,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: 1,
workspaceId: req.body.workspaceId,
workspaceId: projectId,
environment: req.body.environment,
secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
@@ -633,7 +648,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName)
}),
body: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.UPDATE.workspaceId),
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.workspaceId),
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment),
secretValue: z
.string()
@@ -679,13 +695,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: req.body.projectSlug,
projectId: req.body.workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
const secretOperation = await server.services.secret.updateSecretRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
environment: req.body.environment,
projectId: req.body.workspaceId,
projectId,
secretPath: req.body.secretPath,
secretName: req.params.secretName,
type: req.body.type,
@@ -707,7 +732,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({
projectId: req.body.workspaceId,
projectId,
...req.auditLogInfo,
event: {
type: EventType.UPDATE_SECRET,
@@ -727,7 +752,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: 1,
workspaceId: req.body.workspaceId,
workspaceId: projectId,
environment: req.body.environment,
secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
@@ -757,7 +782,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName)
}),
body: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.DELETE.workspaceId),
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId),
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment),
secretPath: z
.string()
@@ -780,13 +806,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: req.body.projectSlug,
projectId: req.body.workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
const secretOperation = await server.services.secret.deleteSecretRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
environment: req.body.environment,
projectId: req.body.workspaceId,
projectId,
secretPath: req.body.secretPath,
secretName: req.params.secretName,
type: req.body.type
@@ -798,7 +833,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({
projectId: req.body.workspaceId,
projectId,
...req.auditLogInfo,
event: {
type: EventType.DELETE_SECRET,
@@ -817,7 +852,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: 1,
workspaceId: req.body.workspaceId,
workspaceId: projectId,
environment: req.body.environment,
secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
@@ -14,7 +14,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
method: "POST",
config: {
rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
})
},
schema: {
@@ -55,7 +55,9 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
url: "/email/verify",
method: "POST",
config: {
rateLimit: authRateLimit
rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) || req.realIp
})
},
schema: {
body: z.object({
@@ -26,7 +26,8 @@ export enum AppConnection {
Heroku = "heroku",
Render = "render",
Flyio = "flyio",
GitLab = "gitlab"
GitLab = "gitlab",
Cloudflare = "cloudflare"
}
export enum AWSRegion {
@@ -51,6 +51,11 @@ import {
validateAzureKeyVaultConnectionCredentials
} from "./azure-key-vault";
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
import {
getCloudflareConnectionListItem,
validateCloudflareConnectionCredentials
} from "./cloudflare/cloudflare-connection-fns";
import {
DatabricksConnectionMethod,
getDatabricksConnectionListItem,
@@ -130,7 +135,8 @@ export const listAppConnectionOptions = () => {
getHerokuConnectionListItem(),
getRenderConnectionListItem(),
getFlyioConnectionListItem(),
getGitLabConnectionListItem()
getGitLabConnectionListItem(),
getCloudflareConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -209,7 +215,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Heroku]: validateHerokuConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -245,6 +252,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case TerraformCloudConnectionMethod.ApiToken:
case VercelConnectionMethod.ApiToken:
case OnePassConnectionMethod.ApiToken:
case CloudflareConnectionMethod.APIToken:
return "API Token";
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:
@@ -323,7 +331,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Heroku]: platformManagedCredentialsNotSupported,
[AppConnection.Render]: platformManagedCredentialsNotSupported,
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
[AppConnection.GitLab]: platformManagedCredentialsNotSupported
[AppConnection.GitLab]: platformManagedCredentialsNotSupported,
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported
};
export const enterpriseAppCheck = async (
@@ -28,7 +28,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Heroku]: "Heroku",
[AppConnection.Render]: "Render",
[AppConnection.Flyio]: "Fly.io",
[AppConnection.GitLab]: "GitLab"
[AppConnection.GitLab]: "GitLab",
[AppConnection.Cloudflare]: "Cloudflare"
};
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -59,5 +60,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Heroku]: AppConnectionPlanType.Regular,
[AppConnection.Render]: AppConnectionPlanType.Regular,
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
[AppConnection.GitLab]: AppConnectionPlanType.Regular
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular
};
@@ -47,6 +47,8 @@ import { azureDevOpsConnectionService } from "./azure-devops/azure-devops-servic
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
import { camundaConnectionService } from "./camunda/camunda-connection-service";
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
import { databricksConnectionService } from "./databricks/databricks-connection-service";
import { ValidateFlyioConnectionCredentialsSchema } from "./flyio";
@@ -116,7 +118,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Heroku]: ValidateHerokuConnectionCredentialsSchema,
[AppConnection.Render]: ValidateRenderConnectionCredentialsSchema,
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@@ -525,6 +528,7 @@ export const appConnectionServiceFactory = ({
heroku: herokuConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
render: renderConnectionService(connectAppConnectionById),
flyio: flyioConnectionService(connectAppConnectionById),
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
cloudflare: cloudflareConnectionService(connectAppConnectionById)
};
};
@@ -62,6 +62,12 @@ import {
TCamundaConnectionInput,
TValidateCamundaConnectionCredentialsSchema
} from "./camunda";
import {
TCloudflareConnection,
TCloudflareConnectionConfig,
TCloudflareConnectionInput,
TValidateCloudflareConnectionCredentialsSchema
} from "./cloudflare/cloudflare-connection-types";
import {
TDatabricksConnection,
TDatabricksConnectionConfig,
@@ -189,6 +195,7 @@ export type TAppConnection = { id: string } & (
| TRenderConnection
| TFlyioConnection
| TGitLabConnection
| TCloudflareConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -224,6 +231,7 @@ export type TAppConnectionInput = { id: string } & (
| TRenderConnectionInput
| TFlyioConnectionInput
| TGitLabConnectionInput
| TCloudflareConnectionInput
);
export type TSqlConnectionInput =
@@ -266,7 +274,8 @@ export type TAppConnectionConfig =
| THerokuConnectionConfig
| TRenderConnectionConfig
| TFlyioConnectionConfig
| TGitLabConnectionConfig;
| TGitLabConnectionConfig
| TCloudflareConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -296,7 +305,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateHerokuConnectionCredentialsSchema
| TValidateRenderConnectionCredentialsSchema
| TValidateFlyioConnectionCredentialsSchema
| TValidateGitLabConnectionCredentialsSchema;
| TValidateGitLabConnectionCredentialsSchema
| TValidateCloudflareConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
@@ -0,0 +1,3 @@
export enum CloudflareConnectionMethod {
APIToken = "api-token"
}
@@ -0,0 +1,75 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { CloudflareConnectionMethod } from "./cloudflare-connection-enum";
import {
TCloudflareConnection,
TCloudflareConnectionConfig,
TCloudflarePagesProject
} from "./cloudflare-connection-types";
export const getCloudflareConnectionListItem = () => {
return {
name: "Cloudflare" as const,
app: AppConnection.Cloudflare as const,
methods: Object.values(CloudflareConnectionMethod) as [CloudflareConnectionMethod.APIToken]
};
};
export const listCloudflarePagesProjects = async (
appConnection: TCloudflareConnection
): Promise<TCloudflarePagesProject[]> => {
const {
credentials: { apiToken, accountId }
} = appConnection;
const { data } = await request.get<{ result: { name: string; id: string }[] }>(
`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/accounts/${accountId}/pages/projects`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
}
);
return data.result.map((a) => ({
name: a.name,
id: a.id
}));
};
export const validateCloudflareConnectionCredentials = async (config: TCloudflareConnectionConfig) => {
const { apiToken, accountId } = config.credentials;
try {
const resp = await request.get(`${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/accounts/${accountId}`, {
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
});
if (resp.data === null) {
throw new BadRequestError({
message: "Unable to validate connection: Invalid API token provided."
});
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
message: `Failed to validate credentials: ${error.response?.data?.errors?.[0]?.message || error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to validate connection: verify credentials"
});
}
return config.credentials;
};
@@ -0,0 +1,74 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { CloudflareConnectionMethod } from "./cloudflare-connection-enum";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
const accountIdCharacterValidator = characterValidator([
CharacterType.AlphaNumeric,
CharacterType.Underscore,
CharacterType.Hyphen
]);
export const CloudflareConnectionApiTokenCredentialsSchema = z.object({
accountId: z
.string()
.trim()
.min(1, "Account ID required")
.max(256, "Account ID cannot exceed 256 characters")
.refine(
(val) => accountIdCharacterValidator(val),
"Account ID can only contain alphanumeric characters, underscores, and hyphens"
),
apiToken: z.string().trim().min(1, "API token required").max(256, "API token cannot exceed 256 characters")
});
const BaseCloudflareConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Cloudflare) });
export const CloudflareConnectionSchema = BaseCloudflareConnectionSchema.extend({
method: z.literal(CloudflareConnectionMethod.APIToken),
credentials: CloudflareConnectionApiTokenCredentialsSchema
});
export const SanitizedCloudflareConnectionSchema = z.discriminatedUnion("method", [
BaseCloudflareConnectionSchema.extend({
method: z.literal(CloudflareConnectionMethod.APIToken),
credentials: CloudflareConnectionApiTokenCredentialsSchema.pick({ accountId: true })
})
]);
export const ValidateCloudflareConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(CloudflareConnectionMethod.APIToken)
.describe(AppConnections.CREATE(AppConnection.Cloudflare).method),
credentials: CloudflareConnectionApiTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Cloudflare).credentials
)
})
]);
export const CreateCloudflareConnectionSchema = ValidateCloudflareConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Cloudflare)
);
export const UpdateCloudflareConnectionSchema = z
.object({
credentials: CloudflareConnectionApiTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Cloudflare).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Cloudflare));
export const CloudflareConnectionListItemSchema = z.object({
name: z.literal("Cloudflare"),
app: z.literal(AppConnection.Cloudflare),
methods: z.nativeEnum(CloudflareConnectionMethod).array()
});
@@ -0,0 +1,30 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listCloudflarePagesProjects } from "./cloudflare-connection-fns";
import { TCloudflareConnection } from "./cloudflare-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TCloudflareConnection>;
export const cloudflareConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listPagesProjects = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Cloudflare, connectionId, actor);
try {
const projects = await listCloudflarePagesProjects(appConnection);
return projects;
} catch (error) {
logger.error(error, "Failed to list Cloudflare Pages projects for Cloudflare connection");
return [];
}
};
return {
listPagesProjects
};
};
@@ -0,0 +1,30 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CloudflareConnectionSchema,
CreateCloudflareConnectionSchema,
ValidateCloudflareConnectionCredentialsSchema
} from "./cloudflare-connection-schema";
export type TCloudflareConnection = z.infer<typeof CloudflareConnectionSchema>;
export type TCloudflareConnectionInput = z.infer<typeof CreateCloudflareConnectionSchema> & {
app: AppConnection.Cloudflare;
};
export type TValidateCloudflareConnectionCredentialsSchema = typeof ValidateCloudflareConnectionCredentialsSchema;
export type TCloudflareConnectionConfig = DiscriminativePick<
TCloudflareConnectionInput,
"method" | "app" | "credentials"
> & {
orgId: string;
};
export type TCloudflarePagesProject = {
id: string;
name: string;
};
@@ -7,6 +7,7 @@ import { request } from "@app/lib/config/request";
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { getInstanceIntegrationsConfig } from "@app/services/super-admin/super-admin-service";
import { AppConnection } from "../app-connection-enums";
import { GitHubConnectionMethod } from "./github-connection-enums";
@@ -14,13 +15,14 @@ import { TGitHubConnection, TGitHubConnectionConfig } from "./github-connection-
export const getGitHubConnectionListItem = () => {
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
return {
name: "GitHub" as const,
app: AppConnection.GitHub as const,
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG
appClientSlug: gitHubAppConnection.appSlug || INF_APP_CONNECTION_GITHUB_APP_SLUG
};
};
@@ -30,23 +32,24 @@ export const getGitHubClient = (appConnection: TGitHubConnection) => {
const { method, credentials } = appConnection;
let client: Octokit;
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
const appId = gitHubAppConnection.appId || appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
const appPrivateKey = gitHubAppConnection.privateKey || appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
switch (method) {
case GitHubConnectionMethod.App:
if (!appCfg.INF_APP_CONNECTION_GITHUB_APP_ID || !appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY) {
if (!appId || !appPrivateKey) {
throw new InternalServerError({
message: `GitHub ${getAppConnectionMethodName(method).replace(
"GitHub",
""
)} environment variables have not been configured`
message: `GitHub ${getAppConnectionMethodName(method).replace("GitHub", "")} has not been configured`
});
}
client = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: appCfg.INF_APP_CONNECTION_GITHUB_APP_ID,
privateKey: appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY,
appId,
privateKey: appPrivateKey,
installationId: credentials.installationId
}
});
@@ -154,6 +157,8 @@ type TokenRespData = {
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
const { credentials, method } = config;
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
const {
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
@@ -165,8 +170,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
const { clientId, clientSecret } =
method === GitHubConnectionMethod.App
? {
clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
clientId: gitHubAppConnection.clientId || INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
clientSecret: gitHubAppConnection.clientSecret || INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
}
: // oauth
{
@@ -84,6 +84,8 @@ export enum IntegrationUrls {
QOVERY_API_URL = "https://api.qovery.com",
TERRAFORM_CLOUD_API_URL = "https://app.terraform.io",
CLOUDFLARE_PAGES_API_URL = "https://api.cloudflare.com",
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
CLOUDFLARE_API_URL = "https://api.cloudflare.com",
// eslint-disable-next-line
CLOUDFLARE_WORKERS_API_URL = "https://api.cloudflare.com",
BITBUCKET_API_URL = "https://api.bitbucket.org",
@@ -42,7 +42,7 @@ import { TProjectPermission } from "@app/lib/types";
import { TQueueServiceFactory } from "@app/queue";
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { ActorType } from "../auth/auth-type";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { TCertificateDALFactory } from "../certificate/certificate-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { expandInternalCa } from "../certificate-authority/certificate-authority-fns";
@@ -82,6 +82,7 @@ import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } f
import { TProjectQueueFactory } from "./project-queue";
import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
import {
ProjectFilterType,
TCreateProjectDTO,
TDeleteProjectDTO,
TDeleteProjectWorkflowIntegration,
@@ -866,6 +867,39 @@ export const projectServiceFactory = ({
});
};
const extractProjectIdFromSlug = async ({
projectSlug,
projectId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: {
projectSlug?: string;
projectId?: string;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actor: ActorType;
actorOrgId: string;
}) => {
if (projectId) return projectId;
if (!projectSlug) throw new BadRequestError({ message: "You must provide projectSlug or workspaceId" });
const project = await getAProject({
filter: {
type: ProjectFilterType.SLUG,
orgId: actorOrgId,
slug: projectSlug
},
actorId,
actorAuthMethod,
actor,
actorOrgId
});
if (!project) throw new NotFoundError({ message: `No project found with slug ${projectSlug}` });
return project.id;
};
const getProjectUpgradeStatus = async ({
projectId,
actor,
@@ -2006,6 +2040,7 @@ export const projectServiceFactory = ({
getProjectSshConfig,
updateProjectSshConfig,
requestProjectAccess,
searchProjects
searchProjects,
extractProjectIdFromSlug
};
};
@@ -6,6 +6,7 @@ import { ActionProjectType, TSecretFoldersInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { PgSqlLock } from "@app/keystore/keystore";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
@@ -83,36 +84,75 @@ export const secretFolderServiceFactory = ({
// that is this request must be idempotent
// so we do a tricky move. we try to find the to be created folder path if that is exactly match return that
// else we get some path before that then we will start creating remaining folder
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.CreateFolder(env.id, env.projectId)]);
const pathWithFolder = path.join(secretPath, name);
const parentFolder = await folderDAL.findClosestFolder(projectId, environment, pathWithFolder, tx);
// no folder found is not possible root should be their
if (!parentFolder) {
throw new NotFoundError({
message: `Folder with path '${pathWithFolder}' in environment with slug '${environment}' not found`
message: `Parent folder for path '${pathWithFolder}' not found`
});
}
// exact folder
if (parentFolder.path === pathWithFolder) return parentFolder;
let parentFolderId = parentFolder.id;
// check if the exact folder already exists
const existingFolder = await folderDAL.findOne(
{
envId: env.id,
parentId: parentFolder.id,
name,
isReserved: false
},
tx
);
if (existingFolder) {
return existingFolder;
}
// exact folder case
if (parentFolder.path === pathWithFolder) {
return parentFolder;
}
let currentParentId = parentFolder.id;
// build the full path we need by processing each segment
if (parentFolder.path !== secretPath) {
// this is upsert folder in a path
// we are not taking snapshots of this because
// snapshot will be removed from automatic for all commits to user click or cron based
const missingSegment = secretPath.substring(parentFolder.path.length).split("/").filter(Boolean);
if (missingSegment.length) {
const newFolders: Array<TSecretFoldersInsert & { id: string }> = missingSegment.map((segment) => {
const missingSegments = secretPath.substring(parentFolder.path.length).split("/").filter(Boolean);
const newFolders: TSecretFoldersInsert[] = [];
// process each segment sequentially
for await (const segment of missingSegments) {
const existingSegment = await folderDAL.findOne(
{
name: segment,
parentId: currentParentId,
envId: env.id,
isReserved: false
},
tx
);
if (existingSegment) {
// use existing folder and update the path / parent
currentParentId = existingSegment.id;
} else {
const newFolder = {
name: segment,
parentId: parentFolderId,
parentId: currentParentId,
id: uuidv4(),
envId: env.id,
version: 1
};
parentFolderId = newFolder.id;
return newFolder;
});
parentFolderId = newFolders.at(-1)?.id as string;
currentParentId = newFolder.id;
newFolders.push(newFolder);
}
}
if (newFolders.length) {
const docs = await folderDAL.insertMany(newFolders, tx);
const folderVersions = await folderVersionDAL.insertMany(
docs.map((doc) => ({
@@ -133,7 +173,7 @@ export const secretFolderServiceFactory = ({
}
},
message: "Folder created",
folderId: parentFolderId,
folderId: currentParentId,
changes: folderVersions.map((fv) => ({
type: CommitType.ADD,
folderVersionId: fv.id
@@ -145,9 +185,10 @@ export const secretFolderServiceFactory = ({
}
const doc = await folderDAL.create(
{ name, envId: env.id, version: 1, parentId: parentFolderId, description },
{ name, envId: env.id, version: 1, parentId: currentParentId, description },
tx
);
const folderVersion = await folderVersionDAL.create(
{
name: doc.name,
@@ -158,6 +199,7 @@ export const secretFolderServiceFactory = ({
},
tx
);
await folderCommitService.createCommit(
{
actor: {
@@ -167,7 +209,7 @@ export const secretFolderServiceFactory = ({
}
},
message: "Folder created",
folderId: parentFolderId,
folderId: doc.id,
changes: [
{
type: CommitType.ADD,
@@ -177,6 +219,7 @@ export const secretFolderServiceFactory = ({
},
tx
);
return doc;
});
@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const CLOUDFLARE_PAGES_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Cloudflare Pages",
destination: SecretSync.CloudflarePages,
connection: AppConnection.Cloudflare,
canImportSecrets: false
};
@@ -0,0 +1,138 @@
import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
import { TCloudflarePagesSyncWithCredentials } from "./cloudflare-pages-types";
const getProjectEnvironmentSecrets = async (secretSync: TCloudflarePagesSyncWithCredentials) => {
const {
destinationConfig,
connection: {
credentials: { apiToken, accountId }
}
} = secretSync;
const secrets = (
await request.get<{
result: {
deployment_configs: Record<
string,
{
env_vars: Record<string, { type: "plain_text" | "secret_text"; value: string }>;
}
>;
};
}>(
`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects/${destinationConfig.projectName}`,
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
}
)
).data.result.deployment_configs[destinationConfig.environment].env_vars;
return Object.entries(secrets ?? {}).map(([key, envVar]) => ({
key,
value: envVar.value
}));
};
export const CloudflarePagesSyncFns = {
syncSecrets: async (secretSync: TCloudflarePagesSyncWithCredentials, secretMap: TSecretMap) => {
const {
destinationConfig,
connection: {
credentials: { apiToken, accountId }
}
} = secretSync;
// Create/update secret entries
let secretEntries: [string, object | null][] = Object.entries(secretMap).map(([key, val]) => [
key,
{ type: "secret_text", value: val.value }
]);
// Handle deletions if not disabled
if (!secretSync.syncOptions.disableSecretDeletion) {
const existingSecrets = await getProjectEnvironmentSecrets(secretSync);
const toDeleteKeys = existingSecrets
.filter(
(secret) =>
matchesSchema(secret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema) &&
!secretMap[secret.key]
)
.map((secret) => secret.key);
const toDeleteEntries: [string, null][] = toDeleteKeys.map((key) => [key, null]);
secretEntries = [...secretEntries, ...toDeleteEntries];
}
const data = {
deployment_configs: {
[destinationConfig.environment]: {
env_vars: Object.fromEntries(secretEntries)
}
}
};
await request.patch(
`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects/${destinationConfig.projectName}`,
data,
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
}
);
},
getSecrets: async (secretSync: TCloudflarePagesSyncWithCredentials): Promise<TSecretMap> => {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
},
removeSecrets: async (secretSync: TCloudflarePagesSyncWithCredentials, secretMap: TSecretMap) => {
const {
destinationConfig,
connection: {
credentials: { apiToken, accountId }
}
} = secretSync;
const secrets = await getProjectEnvironmentSecrets(secretSync);
const toDeleteKeys = secrets
.filter(
(secret) =>
matchesSchema(secret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema) &&
secret.key in secretMap
)
.map((secret) => secret.key);
if (toDeleteKeys.length === 0) return;
const secretEntries: [string, null][] = toDeleteKeys.map((key) => [key, null]);
const data = {
deployment_configs: {
[destinationConfig.environment]: {
env_vars: Object.fromEntries(secretEntries)
}
}
};
await request.patch(
`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects/${destinationConfig.projectName}`,
data,
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
}
);
}
};
@@ -0,0 +1,53 @@
import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const CloudflarePagesSyncDestinationConfigSchema = z.object({
projectName: z
.string()
.min(1, "Project name is required")
.describe(SecretSyncs.DESTINATION_CONFIG.CLOUDFLARE_PAGES.projectName),
environment: z
.string()
.min(1, "Environment is required")
.describe(SecretSyncs.DESTINATION_CONFIG.CLOUDFLARE_PAGES.environment)
});
const CloudflarePagesSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
export const CloudflarePagesSyncSchema = BaseSecretSyncSchema(
SecretSync.CloudflarePages,
CloudflarePagesSyncOptionsConfig
).extend({
destination: z.literal(SecretSync.CloudflarePages),
destinationConfig: CloudflarePagesSyncDestinationConfigSchema
});
export const CreateCloudflarePagesSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.CloudflarePages,
CloudflarePagesSyncOptionsConfig
).extend({
destinationConfig: CloudflarePagesSyncDestinationConfigSchema
});
export const UpdateCloudflarePagesSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.CloudflarePages,
CloudflarePagesSyncOptionsConfig
).extend({
destinationConfig: CloudflarePagesSyncDestinationConfigSchema.optional()
});
export const CloudflarePagesSyncListItemSchema = z.object({
name: z.literal("Cloudflare Pages"),
connection: z.literal(AppConnection.Cloudflare),
destination: z.literal(SecretSync.CloudflarePages),
canImportSecrets: z.literal(false)
});
@@ -0,0 +1,19 @@
import z from "zod";
import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types";
import {
CloudflarePagesSyncListItemSchema,
CloudflarePagesSyncSchema,
CreateCloudflarePagesSyncSchema
} from "./cloudflare-pages-schema";
export type TCloudflarePagesSyncListItem = z.infer<typeof CloudflarePagesSyncListItemSchema>;
export type TCloudflarePagesSync = z.infer<typeof CloudflarePagesSyncSchema>;
export type TCloudflarePagesSyncInput = z.infer<typeof CreateCloudflarePagesSyncSchema>;
export type TCloudflarePagesSyncWithCredentials = TCloudflarePagesSync & {
connection: TCloudflareConnection;
};
@@ -19,7 +19,8 @@ export enum SecretSync {
Heroku = "heroku",
Render = "render",
Flyio = "flyio",
GitLab = "gitlab"
GitLab = "gitlab",
CloudflarePages = "cloudflare-pages"
}
export enum SecretSyncInitialSyncBehavior {
@@ -29,6 +29,8 @@ import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFact
import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops";
import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
import { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants";
import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns";
import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio";
import { GCP_SYNC_LIST_OPTION } from "./gcp";
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
@@ -65,7 +67,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Heroku]: HEROKU_SYNC_LIST_OPTION,
[SecretSync.Render]: RENDER_SYNC_LIST_OPTION,
[SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION,
[SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION
[SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION,
[SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION
};
export const listSecretSyncOptions = () => {
@@ -231,6 +234,8 @@ export const SecretSyncFns = {
return FlyioSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.GitLab:
return GitLabSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
case SecretSync.CloudflarePages:
return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -320,6 +325,9 @@ export const SecretSyncFns = {
case SecretSync.GitLab:
secretMap = await GitLabSyncFns.getSecrets(secretSync);
break;
case SecretSync.CloudflarePages:
secretMap = await CloudflarePagesSyncFns.getSecrets(secretSync);
break;
default:
throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -395,6 +403,8 @@ export const SecretSyncFns = {
return FlyioSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.GitLab:
return GitLabSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
case SecretSync.CloudflarePages:
return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap);
default:
throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -22,7 +22,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Heroku]: "Heroku",
[SecretSync.Render]: "Render",
[SecretSync.Flyio]: "Fly.io",
[SecretSync.GitLab]: "GitLab"
[SecretSync.GitLab]: "GitLab",
[SecretSync.CloudflarePages]: "Cloudflare Pages"
};
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@@ -46,7 +47,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Heroku]: AppConnection.Heroku,
[SecretSync.Render]: AppConnection.Render,
[SecretSync.Flyio]: AppConnection.Flyio,
[SecretSync.GitLab]: AppConnection.GitLab
[SecretSync.GitLab]: AppConnection.GitLab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare
};
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
@@ -70,5 +72,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.Heroku]: SecretSyncPlanType.Regular,
[SecretSync.Render]: SecretSyncPlanType.Regular,
[SecretSync.Flyio]: SecretSyncPlanType.Regular,
[SecretSync.GitLab]: SecretSyncPlanType.Regular
[SecretSync.GitLab]: SecretSyncPlanType.Regular,
[SecretSync.CloudflarePages]: SecretSyncPlanType.Regular
};
@@ -72,6 +72,12 @@ import {
TAzureKeyVaultSyncListItem,
TAzureKeyVaultSyncWithCredentials
} from "./azure-key-vault";
import {
TCloudflarePagesSync,
TCloudflarePagesSyncInput,
TCloudflarePagesSyncListItem,
TCloudflarePagesSyncWithCredentials
} from "./cloudflare-pages/cloudflare-pages-types";
import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types";
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
import { TGitLabSync, TGitLabSyncInput, TGitLabSyncListItem, TGitLabSyncWithCredentials } from "./gitlab";
@@ -129,7 +135,8 @@ export type TSecretSync =
| THerokuSync
| TRenderSync
| TFlyioSync
| TGitLabSync;
| TGitLabSync
| TCloudflarePagesSync;
export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials
@@ -152,7 +159,8 @@ export type TSecretSyncWithCredentials =
| THerokuSyncWithCredentials
| TRenderSyncWithCredentials
| TFlyioSyncWithCredentials
| TGitLabSyncWithCredentials;
| TGitLabSyncWithCredentials
| TCloudflarePagesSyncWithCredentials;
export type TSecretSyncInput =
| TAwsParameterStoreSyncInput
@@ -175,7 +183,8 @@ export type TSecretSyncInput =
| THerokuSyncInput
| TRenderSyncInput
| TFlyioSyncInput
| TGitLabSyncInput;
| TGitLabSyncInput
| TCloudflarePagesSyncInput;
export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem
@@ -198,7 +207,8 @@ export type TSecretSyncListItem =
| THerokuSyncListItem
| TRenderSyncListItem
| TFlyioSyncListItem
| TGitLabSyncListItem;
| TGitLabSyncListItem
| TCloudflarePagesSyncListItem;
export type TSyncOptionsConfig = {
canImportSecrets: boolean;
@@ -1543,9 +1543,8 @@ export const secretServiceFactory = ({
actor,
environment,
viewSecretValue,
projectId: workspaceId,
projectId,
expandSecretReferences,
projectSlug,
actorId,
actorOrgId,
actorAuthMethod,
@@ -1553,7 +1552,6 @@ export const secretServiceFactory = ({
includeImports,
version
}: TGetASecretRawDTO) => {
const projectId = workspaceId || (await projectDAL.findProjectBySlug(projectSlug as string, actorOrgId)).id;
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
const secret = await secretV2BridgeService.getSecretByName({
+1 -2
View File
@@ -229,8 +229,7 @@ export type TGetASecretRawDTO = {
type: "shared" | "personal";
includeImports?: boolean;
version?: number;
projectSlug?: string;
projectId?: string;
projectId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetASecretByIdRawDTO = {
@@ -1,4 +1,5 @@
import bcrypt from "bcrypt";
import { CronJob } from "cron";
import jwt from "jsonwebtoken";
import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
@@ -8,6 +9,7 @@ import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
import { TAuthLoginFactory } from "../auth/auth-login-service";
@@ -35,6 +37,7 @@ import {
TAdminBootstrapInstanceDTO,
TAdminGetIdentitiesDTO,
TAdminGetUsersDTO,
TAdminIntegrationConfig,
TAdminSignUpDTO,
TGetOrganizationsDTO
} from "./super-admin-types";
@@ -70,6 +73,31 @@ export let getServerCfg: () => Promise<
}
>;
let adminIntegrationsConfig: TAdminIntegrationConfig = {
slack: {
clientSecret: "",
clientId: ""
},
microsoftTeams: {
appId: "",
clientSecret: "",
botId: ""
},
gitHubAppConnection: {
clientId: "",
clientSecret: "",
appSlug: "",
appId: "",
privateKey: ""
}
};
Object.freeze(adminIntegrationsConfig);
export const getInstanceIntegrationsConfig = () => {
return adminIntegrationsConfig;
};
const ADMIN_CONFIG_KEY = "infisical-admin-cfg";
const ADMIN_CONFIG_KEY_EXP = 60; // 60s
export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
@@ -138,6 +166,74 @@ export const superAdminServiceFactory = ({
return serverCfg;
};
const getAdminIntegrationsConfig = async () => {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (!serverCfg) {
throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" });
}
const decrypt = kmsService.decryptWithRootKey();
const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : "";
const slackClientSecret = serverCfg.encryptedSlackClientSecret
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
: "";
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
: "";
const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret
? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString()
: "";
const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId
? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString()
: "";
const gitHubAppConnectionClientId = serverCfg.encryptedGitHubAppConnectionClientId
? decrypt(serverCfg.encryptedGitHubAppConnectionClientId).toString()
: "";
const gitHubAppConnectionClientSecret = serverCfg.encryptedGitHubAppConnectionClientSecret
? decrypt(serverCfg.encryptedGitHubAppConnectionClientSecret).toString()
: "";
const gitHubAppConnectionAppSlug = serverCfg.encryptedGitHubAppConnectionSlug
? decrypt(serverCfg.encryptedGitHubAppConnectionSlug).toString()
: "";
const gitHubAppConnectionAppId = serverCfg.encryptedGitHubAppConnectionId
? decrypt(serverCfg.encryptedGitHubAppConnectionId).toString()
: "";
const gitHubAppConnectionAppPrivateKey = serverCfg.encryptedGitHubAppConnectionPrivateKey
? decrypt(serverCfg.encryptedGitHubAppConnectionPrivateKey).toString()
: "";
return {
slack: {
clientSecret: slackClientSecret,
clientId: slackClientId
},
microsoftTeams: {
appId: microsoftAppId,
clientSecret: microsoftClientSecret,
botId: microsoftBotId
},
gitHubAppConnection: {
clientId: gitHubAppConnectionClientId,
clientSecret: gitHubAppConnectionClientSecret,
appSlug: gitHubAppConnectionAppSlug,
appId: gitHubAppConnectionAppId,
privateKey: gitHubAppConnectionAppPrivateKey
}
};
};
const $syncAdminIntegrationConfig = async () => {
const config = await getAdminIntegrationsConfig();
Object.freeze(config);
adminIntegrationsConfig = config;
};
const updateServerCfg = async (
data: TSuperAdminUpdate & {
slackClientId?: string;
@@ -145,6 +241,11 @@ export const superAdminServiceFactory = ({
microsoftTeamsAppId?: string;
microsoftTeamsClientSecret?: string;
microsoftTeamsBotId?: string;
gitHubAppConnectionClientId?: string;
gitHubAppConnectionClientSecret?: string;
gitHubAppConnectionSlug?: string;
gitHubAppConnectionId?: string;
gitHubAppConnectionPrivateKey?: string;
},
userId: string
) => {
@@ -236,10 +337,51 @@ export const superAdminServiceFactory = ({
updatedData.microsoftTeamsBotId = undefined;
microsoftTeamsSettingsUpdated = true;
}
let gitHubAppConnectionSettingsUpdated = false;
if (data.gitHubAppConnectionClientId !== undefined) {
const encryptedClientId = encryptWithRoot(Buffer.from(data.gitHubAppConnectionClientId));
updatedData.encryptedGitHubAppConnectionClientId = encryptedClientId;
updatedData.gitHubAppConnectionClientId = undefined;
gitHubAppConnectionSettingsUpdated = true;
}
if (data.gitHubAppConnectionClientSecret !== undefined) {
const encryptedClientSecret = encryptWithRoot(Buffer.from(data.gitHubAppConnectionClientSecret));
updatedData.encryptedGitHubAppConnectionClientSecret = encryptedClientSecret;
updatedData.gitHubAppConnectionClientSecret = undefined;
gitHubAppConnectionSettingsUpdated = true;
}
if (data.gitHubAppConnectionSlug !== undefined) {
const encryptedAppSlug = encryptWithRoot(Buffer.from(data.gitHubAppConnectionSlug));
updatedData.encryptedGitHubAppConnectionSlug = encryptedAppSlug;
updatedData.gitHubAppConnectionSlug = undefined;
gitHubAppConnectionSettingsUpdated = true;
}
if (data.gitHubAppConnectionId !== undefined) {
const encryptedAppId = encryptWithRoot(Buffer.from(data.gitHubAppConnectionId));
updatedData.encryptedGitHubAppConnectionId = encryptedAppId;
updatedData.gitHubAppConnectionId = undefined;
gitHubAppConnectionSettingsUpdated = true;
}
if (data.gitHubAppConnectionPrivateKey !== undefined) {
const encryptedAppPrivateKey = encryptWithRoot(Buffer.from(data.gitHubAppConnectionPrivateKey));
updatedData.encryptedGitHubAppConnectionPrivateKey = encryptedAppPrivateKey;
updatedData.gitHubAppConnectionPrivateKey = undefined;
gitHubAppConnectionSettingsUpdated = true;
}
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, updatedData);
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
if (gitHubAppConnectionSettingsUpdated) {
await $syncAdminIntegrationConfig();
}
if (
updatedServerCfg.encryptedMicrosoftTeamsAppId &&
updatedServerCfg.encryptedMicrosoftTeamsClientSecret &&
@@ -593,43 +735,6 @@ export const superAdminServiceFactory = ({
await userDAL.updateById(userId, { superAdmin: true });
};
const getAdminIntegrationsConfig = async () => {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (!serverCfg) {
throw new NotFoundError({ name: "AdminConfig", message: "Admin config not found" });
}
const decrypt = kmsService.decryptWithRootKey();
const slackClientId = serverCfg.encryptedSlackClientId ? decrypt(serverCfg.encryptedSlackClientId).toString() : "";
const slackClientSecret = serverCfg.encryptedSlackClientSecret
? decrypt(serverCfg.encryptedSlackClientSecret).toString()
: "";
const microsoftAppId = serverCfg.encryptedMicrosoftTeamsAppId
? decrypt(serverCfg.encryptedMicrosoftTeamsAppId).toString()
: "";
const microsoftClientSecret = serverCfg.encryptedMicrosoftTeamsClientSecret
? decrypt(serverCfg.encryptedMicrosoftTeamsClientSecret).toString()
: "";
const microsoftBotId = serverCfg.encryptedMicrosoftTeamsBotId
? decrypt(serverCfg.encryptedMicrosoftTeamsBotId).toString()
: "";
return {
slack: {
clientSecret: slackClientSecret,
clientId: slackClientId
},
microsoftTeams: {
appId: microsoftAppId,
clientSecret: microsoftClientSecret,
botId: microsoftBotId
}
};
};
const getConfiguredEncryptionStrategies = async () => {
const appCfg = getConfig();
@@ -696,6 +801,19 @@ export const superAdminServiceFactory = ({
return (await keyStore.getItem("invalidating-cache")) !== null;
};
const initializeAdminIntegrationConfigSync = async () => {
logger.info("Setting up background sync process for admin integrations config");
// initial sync upon startup
await $syncAdminIntegrationConfig();
// sync admin integrations config every 5 minutes
const job = new CronJob("*/5 * * * *", $syncAdminIntegrationConfig);
job.start();
return job;
};
return {
initServerCfg,
updateServerCfg,
@@ -714,6 +832,7 @@ export const superAdminServiceFactory = ({
checkIfInvalidatingCache,
getOrganizations,
deleteOrganization,
deleteOrganizationMembership
deleteOrganizationMembership,
initializeAdminIntegrationConfigSync
};
};
@@ -55,3 +55,22 @@ export enum CacheType {
ALL = "all",
SECRETS = "secrets"
}
export type TAdminIntegrationConfig = {
slack: {
clientSecret: string;
clientId: string;
};
microsoftTeams: {
appId: string;
clientSecret: string;
botId: string;
};
gitHubAppConnection: {
clientId: string;
clientSecret: string;
appSlug: string;
appId: string;
privateKey: string;
};
};