Compare commits
87 Commits
revert-cli
...
daniel/fix
Author | SHA1 | Date | |
---|---|---|---|
|
aac63d3097 | ||
|
3d2465ae41 | ||
|
f4f34802bc | ||
|
59cc857aef | ||
|
a6713b2f76 | ||
|
3c9a7c77ff | ||
|
f1bfea61d0 | ||
|
144ad2f25f | ||
|
42aaddccd5 | ||
|
39abeaaab5 | ||
|
b336c0c3d6 | ||
|
305f2d79de | ||
|
d4a6faa92c | ||
|
4800e9c36e | ||
|
842a2e9a06 | ||
|
de81d2d380 | ||
|
f5d769fa05 | ||
|
b3ace353ce | ||
|
48353ab201 | ||
|
2137d13157 | ||
|
647e13d654 | ||
|
bb2a933a39 | ||
|
6f75debb9c | ||
|
90588bc3c9 | ||
|
4a09fc5e63 | ||
|
f0ec8c883f | ||
|
8024d7448f | ||
|
c65b79e00d | ||
|
f5238598aa | ||
|
982aa80092 | ||
|
b30706607f | ||
|
2a3d19dcb2 | ||
|
b4ff620b44 | ||
|
23f1888123 | ||
|
7764f63299 | ||
|
cb3365afd4 | ||
|
58705ffc3f | ||
|
67e57d8993 | ||
|
90ff13a6b5 | ||
|
36145a15c1 | ||
|
4f64ed6b42 | ||
|
d47959ca83 | ||
|
3b2953ca58 | ||
|
1daa503e0e | ||
|
d69e8d2a8d | ||
|
7c7af347fc | ||
|
f85efdc6f8 | ||
|
8680c52412 | ||
|
0ad3c67f82 | ||
|
f75fff0565 | ||
|
1fa1d0a15a | ||
|
e5a967b918 | ||
|
3cfe2223b6 | ||
|
a43d4fd430 | ||
|
80b6fb677c | ||
|
5bc8acd0a7 | ||
|
2575845df7 | ||
|
641d58c157 | ||
|
430f5d516c | ||
|
5cec194e74 | ||
|
5ede4f6f4b | ||
|
4d3581f835 | ||
|
665f7fa5c3 | ||
|
9f4b1d2565 | ||
|
59e2a20180 | ||
|
4fee5a5839 | ||
|
61e245ea58 | ||
|
8d6712aa58 | ||
|
a767870ad6 | ||
|
a0c432628a | ||
|
08a74a63b5 | ||
|
8329240822 | ||
|
ec3cbb9460 | ||
|
57e97a146b | ||
|
9ac4453523 | ||
|
d2c7ed62d0 | ||
|
7e9743b4c2 | ||
|
34cf544b3a | ||
|
12fd063cd5 | ||
|
8fb6063686 | ||
|
459b262865 | ||
|
f27d4ee973 | ||
|
7a13c27055 | ||
|
e7ac783b10 | ||
|
a8eb72a8c5 | ||
|
f76d3e2a14 | ||
|
01ef498397 |
@@ -45,3 +45,4 @@ cli/detect/config/gitleaks.toml:gcp-api-key:582
|
||||
.github/workflows/helm-release-infisical-core.yml:generic-api-key:48
|
||||
.github/workflows/helm-release-infisical-core.yml:generic-api-key:47
|
||||
backend/src/services/smtp/smtp-service.ts:generic-api-key:79
|
||||
frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx:cloudflare-api-key:7
|
||||
|
@@ -26,6 +26,7 @@ export const mockQueue = (): TQueueServiceFactory => {
|
||||
getRepeatableJobs: async () => [],
|
||||
clearQueue: async () => {},
|
||||
stopJobById: async () => {},
|
||||
stopJobByIdPg: async () => {},
|
||||
stopRepeatableJobByJobId: async () => true,
|
||||
stopRepeatableJobByKey: async () => true
|
||||
};
|
||||
|
4
backend/src/@types/fastify.d.ts
vendored
@@ -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";
|
||||
|
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
@@ -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 }>>;
|
||||
};
|
||||
|
133
backend/src/ee/services/dynamic-secret/providers/github.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -477,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",
|
||||
@@ -495,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", [
|
||||
@@ -516,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 = {
|
||||
|
@@ -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
|
||||
|
@@ -2401,6 +2401,10 @@ export const SecretSyncs = {
|
||||
},
|
||||
FLYIO: {
|
||||
appId: "The ID of the Fly.io app to sync secrets to."
|
||||
},
|
||||
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."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -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;
|
||||
|
@@ -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 }[];
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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", {
|
||||
|
@@ -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()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@@ -80,6 +80,10 @@ import {
|
||||
WindmillConnectionListItemSchema
|
||||
} from "@app/services/app-connection/windmill";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import {
|
||||
CloudflareConnectionListItemSchema,
|
||||
SanitizedCloudflareConnectionSchema
|
||||
} from "@app/services/app-connection/cloudflare/cloudflare-connection-schema";
|
||||
|
||||
// can't use discriminated due to multiple schemas for certain apps
|
||||
const SanitizedAppConnectionSchema = z.union([
|
||||
@@ -109,7 +113,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedOnePassConnectionSchema.options,
|
||||
...SanitizedHerokuConnectionSchema.options,
|
||||
...SanitizedRenderConnectionSchema.options,
|
||||
...SanitizedFlyioConnectionSchema.options
|
||||
...SanitizedFlyioConnectionSchema.options,
|
||||
...SanitizedCloudflareConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -139,7 +144,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
OnePassConnectionListItemSchema,
|
||||
HerokuConnectionListItemSchema,
|
||||
RenderConnectionListItemSchema,
|
||||
FlyioConnectionListItemSchema
|
||||
FlyioConnectionListItemSchema,
|
||||
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;
|
||||
}
|
||||
});
|
||||
};
|
@@ -27,6 +27,7 @@ import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
|
||||
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
|
||||
import { registerVercelConnectionRouter } from "./vercel-connection-router";
|
||||
import { registerWindmillConnectionRouter } from "./windmill-connection-router";
|
||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||
|
||||
export * from "./app-connection-router";
|
||||
|
||||
@@ -58,5 +59,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.OnePass]: registerOnePassConnectionRouter,
|
||||
[AppConnection.Heroku]: registerHerokuConnectionRouter,
|
||||
[AppConnection.Render]: registerRenderConnectionRouter,
|
||||
[AppConnection.Flyio]: registerFlyioConnectionRouter
|
||||
[AppConnection.Flyio]: registerFlyioConnectionRouter,
|
||||
[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";
|
||||
@@ -43,5 +44,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.OnePass]: registerOnePassSyncRouter,
|
||||
[SecretSync.Heroku]: registerHerokuSyncRouter,
|
||||
[SecretSync.Render]: registerRenderSyncRouter,
|
||||
[SecretSync.Flyio]: registerFlyioSyncRouter
|
||||
[SecretSync.Flyio]: registerFlyioSyncRouter,
|
||||
[SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter
|
||||
};
|
||||
|
@@ -34,6 +34,10 @@ import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/se
|
||||
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
||||
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
||||
import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill";
|
||||
import {
|
||||
CloudflarePagesSyncListItemSchema,
|
||||
CloudflarePagesSyncSchema
|
||||
} from "@app/services/secret-sync/cloudflare-pages/cloudflare-pages-schema";
|
||||
|
||||
const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
AwsParameterStoreSyncSchema,
|
||||
@@ -55,7 +59,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
OnePassSyncSchema,
|
||||
HerokuSyncSchema,
|
||||
RenderSyncSchema,
|
||||
FlyioSyncSchema
|
||||
FlyioSyncSchema,
|
||||
CloudflarePagesSyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
@@ -78,7 +83,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
OnePassSyncListItemSchema,
|
||||
HerokuSyncListItemSchema,
|
||||
RenderSyncListItemSchema,
|
||||
FlyioSyncListItemSchema
|
||||
FlyioSyncListItemSchema,
|
||||
CloudflarePagesSyncListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||
|
@@ -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 +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({
|
||||
|
@@ -25,7 +25,8 @@ export enum AppConnection {
|
||||
OnePass = "1password",
|
||||
Heroku = "heroku",
|
||||
Render = "render",
|
||||
Flyio = "flyio"
|
||||
Flyio = "flyio",
|
||||
Cloudflare = "cloudflare"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
@@ -99,6 +99,11 @@ import {
|
||||
validateWindmillConnectionCredentials,
|
||||
WindmillConnectionMethod
|
||||
} from "./windmill";
|
||||
import {
|
||||
getCloudflareConnectionListItem,
|
||||
validateCloudflareConnectionCredentials
|
||||
} from "./cloudflare/cloudflare-connection-fns";
|
||||
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
|
||||
|
||||
export const listAppConnectionOptions = () => {
|
||||
return [
|
||||
@@ -128,7 +133,8 @@ export const listAppConnectionOptions = () => {
|
||||
getOnePassConnectionListItem(),
|
||||
getHerokuConnectionListItem(),
|
||||
getRenderConnectionListItem(),
|
||||
getFlyioConnectionListItem()
|
||||
getFlyioConnectionListItem(),
|
||||
getCloudflareConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@@ -206,7 +212,8 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Heroku]: validateHerokuConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
@@ -241,6 +248,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:
|
||||
@@ -318,7 +326,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.OnePass]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Heroku]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Render]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Flyio]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
export const enterpriseAppCheck = async (
|
||||
|
@@ -27,7 +27,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.OnePass]: "1Password",
|
||||
[AppConnection.Heroku]: "Heroku",
|
||||
[AppConnection.Render]: "Render",
|
||||
[AppConnection.Flyio]: "Fly.io"
|
||||
[AppConnection.Flyio]: "Fly.io",
|
||||
[AppConnection.Cloudflare]: "Cloudflare"
|
||||
};
|
||||
|
||||
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
|
||||
@@ -57,5 +58,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.MySql]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Heroku]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Render]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Flyio]: AppConnectionPlanType.Regular
|
||||
[AppConnection.Flyio]: 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";
|
||||
@@ -113,7 +115,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.OnePass]: ValidateOnePassConnectionCredentialsSchema,
|
||||
[AppConnection.Heroku]: ValidateHerokuConnectionCredentialsSchema,
|
||||
[AppConnection.Render]: ValidateRenderConnectionCredentialsSchema,
|
||||
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema
|
||||
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
|
||||
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -521,6 +524,7 @@ export const appConnectionServiceFactory = ({
|
||||
onepass: onePassConnectionService(connectAppConnectionById),
|
||||
heroku: herokuConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
render: renderConnectionService(connectAppConnectionById),
|
||||
cloudflare: cloudflareConnectionService(connectAppConnectionById),
|
||||
flyio: flyioConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
@@ -153,6 +153,12 @@ import {
|
||||
TWindmillConnectionConfig,
|
||||
TWindmillConnectionInput
|
||||
} from "./windmill";
|
||||
import {
|
||||
TCloudflareConnection,
|
||||
TCloudflareConnectionConfig,
|
||||
TCloudflareConnectionInput,
|
||||
TValidateCloudflareConnectionCredentialsSchema
|
||||
} from "./cloudflare/cloudflare-connection-types";
|
||||
|
||||
export type TAppConnection = { id: string } & (
|
||||
| TAwsConnection
|
||||
@@ -182,6 +188,7 @@ export type TAppConnection = { id: string } & (
|
||||
| THerokuConnection
|
||||
| TRenderConnection
|
||||
| TFlyioConnection
|
||||
| TCloudflareConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -216,6 +223,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| THerokuConnectionInput
|
||||
| TRenderConnectionInput
|
||||
| TFlyioConnectionInput
|
||||
| TCloudflareConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput =
|
||||
@@ -257,7 +265,8 @@ export type TAppConnectionConfig =
|
||||
| TOnePassConnectionConfig
|
||||
| THerokuConnectionConfig
|
||||
| TRenderConnectionConfig
|
||||
| TFlyioConnectionConfig;
|
||||
| TFlyioConnectionConfig
|
||||
| TCloudflareConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -286,7 +295,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateOnePassConnectionCredentialsSchema
|
||||
| TValidateHerokuConnectionCredentialsSchema
|
||||
| TValidateRenderConnectionCredentialsSchema
|
||||
| TValidateFlyioConnectionCredentialsSchema;
|
||||
| TValidateFlyioConnectionCredentialsSchema
|
||||
| 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;
|
||||
};
|
@@ -18,7 +18,8 @@ export enum SecretSync {
|
||||
OnePass = "1password",
|
||||
Heroku = "heroku",
|
||||
Render = "render",
|
||||
Flyio = "flyio"
|
||||
Flyio = "flyio",
|
||||
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";
|
||||
@@ -63,7 +65,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.OnePass]: ONEPASS_SYNC_LIST_OPTION,
|
||||
[SecretSync.Heroku]: HEROKU_SYNC_LIST_OPTION,
|
||||
[SecretSync.Render]: RENDER_SYNC_LIST_OPTION,
|
||||
[SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION
|
||||
[SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION,
|
||||
[SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretSyncOptions = () => {
|
||||
@@ -227,6 +230,8 @@ export const SecretSyncFns = {
|
||||
return RenderSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Flyio:
|
||||
return FlyioSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.CloudflarePages:
|
||||
return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -313,6 +318,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.Flyio:
|
||||
secretMap = await FlyioSyncFns.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}`
|
||||
@@ -386,6 +394,8 @@ export const SecretSyncFns = {
|
||||
return RenderSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.Flyio:
|
||||
return FlyioSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
case SecretSync.CloudflarePages:
|
||||
return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
|
@@ -21,7 +21,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.OnePass]: "1Password",
|
||||
[SecretSync.Heroku]: "Heroku",
|
||||
[SecretSync.Render]: "Render",
|
||||
[SecretSync.Flyio]: "Fly.io"
|
||||
[SecretSync.Flyio]: "Fly.io",
|
||||
[SecretSync.CloudflarePages]: "Cloudflare Pages"
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
@@ -44,7 +45,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.OnePass]: AppConnection.OnePass,
|
||||
[SecretSync.Heroku]: AppConnection.Heroku,
|
||||
[SecretSync.Render]: AppConnection.Render,
|
||||
[SecretSync.Flyio]: AppConnection.Flyio
|
||||
[SecretSync.Flyio]: AppConnection.Flyio,
|
||||
[SecretSync.CloudflarePages]: AppConnection.Cloudflare
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
@@ -67,5 +69,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
[SecretSync.OnePass]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Heroku]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Render]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Flyio]: SecretSyncPlanType.Regular
|
||||
[SecretSync.Flyio]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.CloudflarePages]: SecretSyncPlanType.Regular
|
||||
};
|
||||
|
@@ -106,6 +106,12 @@ import {
|
||||
TTerraformCloudSyncWithCredentials
|
||||
} from "./terraform-cloud";
|
||||
import { TVercelSync, TVercelSyncInput, TVercelSyncListItem, TVercelSyncWithCredentials } from "./vercel";
|
||||
import {
|
||||
TCloudflarePagesSync,
|
||||
TCloudflarePagesSyncInput,
|
||||
TCloudflarePagesSyncListItem,
|
||||
TCloudflarePagesSyncWithCredentials
|
||||
} from "./cloudflare-pages/cloudflare-pages-types";
|
||||
|
||||
export type TSecretSync =
|
||||
| TAwsParameterStoreSync
|
||||
@@ -127,7 +133,8 @@ export type TSecretSync =
|
||||
| TOnePassSync
|
||||
| THerokuSync
|
||||
| TRenderSync
|
||||
| TFlyioSync;
|
||||
| TFlyioSync
|
||||
| TCloudflarePagesSync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
| TAwsParameterStoreSyncWithCredentials
|
||||
@@ -149,7 +156,8 @@ export type TSecretSyncWithCredentials =
|
||||
| TOnePassSyncWithCredentials
|
||||
| THerokuSyncWithCredentials
|
||||
| TRenderSyncWithCredentials
|
||||
| TFlyioSyncWithCredentials;
|
||||
| TFlyioSyncWithCredentials
|
||||
| TCloudflarePagesSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput =
|
||||
| TAwsParameterStoreSyncInput
|
||||
@@ -171,7 +179,8 @@ export type TSecretSyncInput =
|
||||
| TOnePassSyncInput
|
||||
| THerokuSyncInput
|
||||
| TRenderSyncInput
|
||||
| TFlyioSyncInput;
|
||||
| TFlyioSyncInput
|
||||
| TCloudflarePagesSyncInput;
|
||||
|
||||
export type TSecretSyncListItem =
|
||||
| TAwsParameterStoreSyncListItem
|
||||
@@ -193,7 +202,8 @@ export type TSecretSyncListItem =
|
||||
| TOnePassSyncListItem
|
||||
| THerokuSyncListItem
|
||||
| TRenderSyncListItem
|
||||
| TFlyioSyncListItem;
|
||||
| TFlyioSyncListItem
|
||||
| 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({
|
||||
|
@@ -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;
|
||||
};
|
||||
};
|
||||
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/cloudflare/available"
|
||||
---
|
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/cloudflare"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [Cloudflare
|
||||
Connections](/integrations/app-connections/cloudflare) to learn how to obtain
|
||||
the required credentials.
|
||||
</Note>
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/cloudflare/{connectionId}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/cloudflare/{connectionId}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/cloudflare/connection-name/{connectionName}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/cloudflare"
|
||||
---
|
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/cloudflare/{connectionId}"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Check out the configuration docs for [Cloudflare
|
||||
Connections](/integrations/app-connections/cloudflare) to learn how to obtain
|
||||
the required credentials.
|
||||
</Note>
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/secret-syncs/cloudflare-pages"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/secret-syncs/cloudflare-pages/{syncId}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/secret-syncs/cloudflare-pages/{syncId}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/secret-syncs/cloudflare-pages/sync-name/{syncName}"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/secret-syncs/cloudflare-pages"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Remove Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/cloudflare-pages/{syncId}/remove-secrets"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Sync Secrets"
|
||||
openapi: "POST /api/v1/secret-syncs/cloudflare-pages/{syncId}/sync-secrets"
|
||||
---
|
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/secret-syncs/cloudflare-pages/{syncId}"
|
||||
---
|
2257
docs/docs.json
Normal file
@@ -127,8 +127,8 @@ Follow the instructions for your operating system to install the Infisical CLI.
|
||||
|
||||
</Tab>
|
||||
<Tab title="Debian/Ubuntu">
|
||||
Add Infisical repository
|
||||
|
||||
Add Infisical repository
|
||||
|
||||
```console
|
||||
$ curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' \
|
||||
@@ -143,7 +143,7 @@ Follow the instructions for your operating system to install the Infisical CLI.
|
||||
</Tab>
|
||||
<Tab title="Arch Linux">
|
||||
Use the `yay` package manager to install from the [Arch User Repository](https://aur.archlinux.org/packages/infisical-bin)
|
||||
|
||||
|
||||
```console
|
||||
$ yay -S infisical-bin
|
||||
```
|
||||
@@ -187,7 +187,7 @@ We'll now use the Infisical-Vercel integration send secrets from Infisical to Ve
|
||||
|
||||
### Infisical-Vercel integration
|
||||
|
||||
To begin we have to import the Next.js app into Vercel as a project. [Follow these instructions](https://nextjs.org/learn/basics/deploying-nextjs-app/deploy) to deploy the Next.js app to Vercel.
|
||||
To begin we have to import the Next.js app into Vercel as a project. [Follow these instructions](https://vercel.com/docs/frameworks/nextjs) to deploy the Next.js app to Vercel.
|
||||
|
||||
Next, navigate to your project's integrations tab in Infisical and press on the Vercel tile to grant Infisical access to your Vercel account.
|
||||
|
||||
@@ -237,7 +237,7 @@ At this stage, you know how to use the Infisical-Vercel integration to sync prod
|
||||
</Accordion>
|
||||
<Accordion title="Is opting out of end-to-end encryption for the Infisical-Vercel integration safe?">
|
||||
Yes. Your secrets are still encrypted at rest. To note, most secret managers actually don't support end-to-end encryption.
|
||||
|
||||
|
||||
Check out the [security guide](/security/overview).
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
112
docs/documentation/platform/dynamic-secrets/github.mdx
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: "GitHub"
|
||||
description: "Learn how to dynamically generate GitHub App tokens."
|
||||
---
|
||||
|
||||
The Infisical GitHub dynamic secret allows you to generate short-lived tokens for a GitHub App on demand based on service account permissions.
|
||||
|
||||
## Setup GitHub App
|
||||
|
||||
<Steps>
|
||||
<Step title="Create an application on GitHub">
|
||||
Navigate to [GitHub App settings](https://github.com/settings/apps) and click **New GitHub App**.
|
||||
|
||||

|
||||
|
||||
Give the application a name and a homepage URL. These values do not need to be anything specific.
|
||||
|
||||
Disable webhook by unchecking the Active checkbox.
|
||||

|
||||
|
||||
Configure the app's permissions to grant the necessary access for the dynamic secret's short-lived tokens based on your use case.
|
||||
|
||||
Create the GitHub Application.
|
||||

|
||||
|
||||
<Note>
|
||||
If you have a GitHub organization, you can create an application under it
|
||||
in your organization Settings > Developer settings > GitHub Apps > New GitHub App.
|
||||
</Note>
|
||||
</Step>
|
||||
<Step title="Save app credentials">
|
||||
Copy the **App ID** and generate a new **Private Key** for your GitHub Application.
|
||||

|
||||
|
||||
Save these for later steps.
|
||||
</Step>
|
||||
<Step title="Install app">
|
||||
Install your application to whichever repositories and organizations that you want the dynamic secret to access.
|
||||

|
||||
|
||||

|
||||
|
||||
Once you've installed the app, **copy the installation ID** from the URL and save it for later steps.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Set up Dynamic Secrets with GitHub
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Secret Overview Dashboard">
|
||||
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||
</Step>
|
||||
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||

|
||||
</Step>
|
||||
<Step title="Select 'GitHub'">
|
||||

|
||||
</Step>
|
||||
<Step title="Provide the inputs for dynamic secret parameters">
|
||||
<ParamField path="Secret Name" type="string" required>
|
||||
Name by which you want the secret to be referenced
|
||||
</ParamField>
|
||||
<ParamField path="App ID" type="string" required>
|
||||
The ID of the app created in earlier steps.
|
||||
</ParamField>
|
||||
<ParamField path="App Private Key PEM" type="string" required>
|
||||
The Private Key of the app created in earlier steps.
|
||||
</ParamField>
|
||||
<ParamField path="Installation ID" type="string" required>
|
||||
The ID of the installation from earlier steps.
|
||||
</ParamField>
|
||||
</Step>
|
||||
<Step title="Click `Submit`">
|
||||
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||
</Step>
|
||||
|
||||
<Step title="Generate dynamic secrets">
|
||||
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||
|
||||

|
||||

|
||||
|
||||
When generating these secrets, the TTL will be fixed to 1 hour.
|
||||
|
||||

|
||||
|
||||
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Audit or Revoke Leases
|
||||
|
||||
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
|
||||
|
||||
This will allow you to see the expiration time of the lease or delete a lease before its set time to live.
|
||||
|
||||

|
||||
|
||||
<Warning>
|
||||
GitHub App tokens cannot be revoked. As such, revoking a token on Infisical does not invalidate the GitHub token; it remains active until it expires.
|
||||
</Warning>
|
||||
|
||||
## Renew Leases
|
||||
|
||||
<Note>
|
||||
GitHub App tokens cannot be renewed because they are fixed to a lifetime of 1 hour.
|
||||
</Note>
|
@@ -49,11 +49,21 @@ In the following steps, we explore how to install the Infisical PKI Issuer using
|
||||
```
|
||||
</Step>
|
||||
<Step title="Install the Issuer Controller">
|
||||
Install the Infisical PKI Issuer controller into your Kubernetes cluster by running the following command:
|
||||
Install the Infisical PKI Issuer controller into your Kubernetes cluster using one of the following methods:
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical-issuer/main/build/install.yaml
|
||||
```
|
||||
<Tabs>
|
||||
<Tab title="Helm">
|
||||
```bash
|
||||
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
|
||||
helm install infisical-pki-issuer infisical-helm-charts/infisical-pki-issuer
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="kubectl">
|
||||
```bash
|
||||
kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical-issuer/main/build/install.yaml
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step title="Create Kubernetes Secret for Infisical PKI Issuer">
|
||||
Start by creating a Kubernetes `Secret` containing the **Client Secret** from step 1. As mentioned previously, this will be used by the Infisical PKI issuer to authenticate with Infisical.
|
||||
|
BIN
docs/images/app-connections/cloudflare/cloudflare-account-id.png
Normal file
After Width: | Height: | Size: 429 KiB |
After Width: | Height: | Size: 976 KiB |
After Width: | Height: | Size: 607 KiB |
After Width: | Height: | Size: 735 KiB |
After Width: | Height: | Size: 300 KiB |
After Width: | Height: | Size: 270 KiB |
After Width: | Height: | Size: 396 KiB |
After Width: | Height: | Size: 337 KiB |
After Width: | Height: | Size: 610 KiB |
After Width: | Height: | Size: 147 KiB |
BIN
docs/images/platform/dynamic-secrets/github/install-app.png
Normal file
After Width: | Height: | Size: 205 KiB |
BIN
docs/images/platform/dynamic-secrets/github/installation.png
Normal file
After Width: | Height: | Size: 518 KiB |