Compare commits

...

87 Commits

Author SHA1 Message Date
Daniel Hougaard
aac63d3097 fix(docs): sdk and changelog tab not working 2025-06-25 22:32:08 +04:00
Sheen
3d2465ae41 Merge pull request #3825 from Infisical/feat/add-cloudflare-app-connection-and-sync
feat: added cloudflare app connection and secret sync
2025-06-25 00:44:58 +08:00
carlosmonastyrski
f4f34802bc Merge pull request #3816 from Infisical/fix/addProjectSlugToSecretsV3
Add projectSlug parameter on secrets v3 endpoints
2025-06-24 13:28:23 -03:00
Daniel Hougaard
59cc857aef fix: further improve inconsistencies 2025-06-24 19:37:32 +04:00
Daniel Hougaard
a6713b2f76 Merge pull request #3846 from Infisical/daniel/multiple-folders
fix(folders): duplicate folders
2025-06-24 19:04:26 +04:00
Daniel Hougaard
3c9a7c77ff chore: re-add comment 2025-06-24 18:58:03 +04:00
Daniel Hougaard
f1bfea61d0 fix: replace keystore lock with postgres lock 2025-06-24 18:54:18 +04:00
Sheen
144ad2f25f misc: added image for generated token 2025-06-24 14:51:11 +00:00
carlosmonastyrski
42aaddccd5 Lint fix 2025-06-23 23:13:29 -03:00
carlosmonastyrski
39abeaaab5 Small fix on workspaceId variable definition on secret-router 2025-06-23 23:05:12 -03:00
Daniel Hougaard
b336c0c3d6 Update secret-folder-service.ts 2025-06-24 03:33:45 +04:00
Daniel Hougaard
305f2d79de remove unused path 2025-06-24 03:32:18 +04:00
Daniel Hougaard
d4a6faa92c fix(folders): multiple folders being created 2025-06-24 03:24:47 +04:00
carlosmonastyrski
4800e9c36e Address PR comments 2025-06-23 17:45:21 -03:00
Sheen
842a2e9a06 Merge pull request #3834 from Infisical/misc/add-self-serve-for-github-app-connection-setup
misc: add self-serve for github app connection setup
2025-06-24 02:45:51 +08:00
Akhil Mohan
de81d2d380 Merge pull request #3833 from akhilmhdh/feat/pg-queue
feat: migrated dynamic secret to pg queue and corrected service layer
2025-06-23 23:51:06 +05:30
=
f5d769fa05 feat: addressed review comments 2025-06-23 23:38:07 +05:30
Scott Wilson
b3ace353ce Merge pull request #3843 from Infisical/email-verify-more-aggressive-rate-limit
improvement(verify-endpoints): add more aggressive rate limiting to verify endpoints
2025-06-23 10:43:25 -07:00
x032205
48353ab201 Merge pull request #3842 from Infisical/sort-tax-id-dropdown
sort tax ID dropdown
2025-06-23 13:40:01 -04:00
Scott Wilson
2137d13157 improve key check operator 2025-06-23 10:36:09 -07:00
Scott Wilson
647e13d654 improvement: add more aggressive rate limiting to verify endpoints 2025-06-23 10:27:36 -07:00
x032205
bb2a933a39 sort tax ID dropdown 2025-06-23 13:26:54 -04:00
Daniel Hougaard
6f75debb9c Merge pull request #3841 from Infisical/daniel/fix-k8s-dynamic-secret-without-gateway
fix(dynamic-secrets/k8s): fix for SSL when not using gateway
2025-06-23 21:26:20 +04:00
Daniel Hougaard
90588bc3c9 fix(dynamic-secrets/k8s): fix for SSL when not using gateway 2025-06-23 21:18:15 +04:00
Sheen
4a09fc5e63 Merge pull request #3840 from Infisical/doc/added-architecture-doc-for-cloud
doc: architecture for US and EU cloud
2025-06-24 00:53:54 +08:00
Sheen Capadngan
f0ec8c883f misc: addressed comments 2025-06-24 00:52:18 +08:00
Sheen Capadngan
8024d7448f misc: updated docs json 2025-06-23 22:18:50 +08:00
Sheen Capadngan
c65b79e00d Merge remote-tracking branch 'origin/main' into feat/add-cloudflare-app-connection-and-sync 2025-06-23 22:16:09 +08:00
Sheen
f5238598aa misc: updated admin integration picture 2025-06-23 14:12:54 +00:00
Sheen Capadngan
982aa80092 misc: added tabs for admin integrations 2025-06-23 22:05:08 +08:00
Sheen Capadngan
b30706607f misc: changed from for to of 2025-06-23 21:13:59 +08:00
Sheen Capadngan
2a3d19dcb2 misc: finalized title 2025-06-23 19:31:19 +08:00
Sheen Capadngan
b4ff620b44 doc: removed specifics 2025-06-23 19:28:05 +08:00
Sheen Capadngan
23f1888123 misc: added mention of separated AWS accounts 2025-06-23 19:16:08 +08:00
Sheen Capadngan
7764f63299 misc: made terms consistent 2025-06-23 19:12:09 +08:00
Sheen Capadngan
cb3365afd4 misc: removed troubleshooting section 2025-06-23 19:08:36 +08:00
Sheen Capadngan
58705ffc3f doc: removed duplicate permission block 2025-06-23 19:03:50 +08:00
Sheen Capadngan
67e57d8993 doc: added mention of NAT 2025-06-23 19:00:45 +08:00
Sheen Capadngan
90ff13a6b5 doc: architecture for US and EU cloud 2025-06-23 18:49:26 +08:00
Maidul Islam
36145a15c1 Merge pull request #3838 from Infisical/docs-update
upgrade mintlify docs
2025-06-23 03:38:53 -04:00
Vladyslav Matsiiako
4f64ed6b42 upgrade mintlify docs 2025-06-22 17:25:17 -07:00
Scott Wilson
d47959ca83 Merge pull request #3822 from Infisical/approval-ui-revisions
improvements(approval-workflows): Improve Approval Workflow Tables and Add Additional Functionality
2025-06-20 15:25:19 -07:00
Scott Wilson
3b2953ca58 chore: revert license 2025-06-20 12:37:24 -07:00
Scott Wilson
1daa503e0e improvement: add space to users/groups list label 2025-06-20 12:34:20 -07:00
Scott Wilson
d69e8d2a8d deconflict merge 2025-06-20 12:33:37 -07:00
Scott Wilson
7c7af347fc improvements: address feedback and fix bugs 2025-06-20 12:25:28 -07:00
Sheen Capadngan
f85efdc6f8 misc: add auto-sync after config update 2025-06-21 02:57:34 +08:00
Sheen Capadngan
8680c52412 Merge branch 'misc/add-self-serve-for-github-app-connection-setup' of https://github.com/Infisical/infisical into misc/add-self-serve-for-github-app-connection-setup 2025-06-21 02:41:39 +08:00
Sheen Capadngan
0ad3c67f82 misc: minor renames 2025-06-21 02:41:15 +08:00
Sheen
f75fff0565 doc: add image 2025-06-20 18:31:36 +00:00
Sheen Capadngan
1fa1d0a15a misc: add self-serve for github connection setup 2025-06-21 02:23:20 +08:00
Akhil Mohan
e5a967b918 Update license-fns.ts 2025-06-20 23:50:03 +05:30
=
3cfe2223b6 feat: migrated dynamic secret to pg queue and corrected service layer types to non infer version 2025-06-20 23:32:40 +05:30
Sheen Capadngan
a43d4fd430 addressed greptie 2025-06-20 21:02:09 +08:00
Sheen Capadngan
80b6fb677c misc: addressed url issue 2025-06-20 20:52:00 +08:00
Sheen Capadngan
5bc8acd0a7 doc: added api references 2025-06-20 20:46:31 +08:00
Sheen
2575845df7 misc: added images to secret sync doc 2025-06-20 12:36:39 +00:00
Sheen Capadngan
641d58c157 misc: addressed sync overflow issue 2025-06-20 20:23:03 +08:00
Sheen Capadngan
430f5d516c misc: text updates to secret sync 2025-06-20 20:20:10 +08:00
Sheen Capadngan
5cec194e74 misc: initial cloudflare pages sync doc 2025-06-20 20:17:02 +08:00
Sheen Capadngan
5ede4f6f4b misc: added placeholder for account ID 2025-06-20 20:08:07 +08:00
Sheen
4d3581f835 doc: added assets for app connection 2025-06-20 12:07:21 +00:00
Sheen Capadngan
665f7fa5c3 misc: updated account ID 2025-06-20 19:50:03 +08:00
Sheen Capadngan
9f4b1d2565 image path updates 2025-06-20 19:42:22 +08:00
Sheen Capadngan
59e2a20180 misc: addressed minor issues 2025-06-20 19:39:33 +08:00
Sheen Capadngan
4fee5a5839 doc: added initial app connection doc 2025-06-20 19:36:27 +08:00
Sheen Capadngan
61e245ea58 Merge remote-tracking branch 'origin/main' into feat/add-cloudflare-app-connection-and-sync 2025-06-20 19:24:45 +08:00
Sheen
8d6712aa58 Merge pull request #3824 from Infisical/doc/add-helm-install-for-pki-issuer
doc: add mention of helm install for pki issuer
2025-06-20 19:20:19 +08:00
Maidul Islam
a767870ad6 Merge pull request #3813 from akhilmhdh/patch/min-knex
feat: added min 0 for knexjs pool
2025-06-19 21:16:08 -04:00
Maidul Islam
a0c432628a Merge pull request #3831 from Infisical/docs/fix-broken-link
Docs links fix
2025-06-19 21:15:22 -04:00
x032205
08a74a63b5 Docs links fix 2025-06-19 21:10:58 -04:00
x032205
8329240822 Merge pull request #3821 from Infisical/ENG-2832
feat(dynamic-secret): Github App Tokens
2025-06-19 21:03:46 -04:00
Maidul Islam
ec3cbb9460 Merge pull request #3830 from Infisical/revert-cli-refresh
Revert CLI refresh PR
2025-06-19 20:58:11 -04:00
Sheen Capadngan
57e97a146b feat: cloudflare pages secret sync 2025-06-20 03:43:36 +08:00
x032205
9ac4453523 Review fixes 2025-06-19 15:12:41 -04:00
Sheen Capadngan
d2c7ed62d0 feat: added cloudflare app connection 2025-06-20 01:16:56 +08:00
Scott Wilson
7e9743b4c2 improvement: standardize and update server side pagination for change requests 2025-06-19 09:39:42 -07:00
Scott Wilson
34cf544b3a fix: correct empty state/search logic 2025-06-19 09:39:42 -07:00
Scott Wilson
12fd063cd5 improvements: minor ui adjustments/additions and pagination for access request table 2025-06-19 09:39:42 -07:00
Scott Wilson
8fb6063686 improvement: better badge color 2025-06-19 09:39:42 -07:00
Scott Wilson
459b262865 improvements: improve approval tables UI and add additional functionality 2025-06-19 09:39:42 -07:00
Sheen Capadngan
f27d4ee973 doc: add mention of helm install for pki issuer 2025-06-19 22:41:39 +08:00
x032205
7a13c27055 Greptile review comments and lint 2025-06-18 18:41:58 -04:00
x032205
e7ac783b10 feat(dynamic-secret): Github App Tokens 2025-06-18 18:33:11 -04:00
carlosmonastyrski
a8eb72a8c5 Fix type issue 2025-06-18 14:48:29 -03:00
carlosmonastyrski
f76d3e2a14 Add projectSlug parameter on secrets v3 endpoints 2025-06-18 14:35:49 -03:00
=
01ef498397 feat: added min 0 for knexjs pool 2025-06-18 15:16:07 +05:30
177 changed files with 6909 additions and 3260 deletions

View File

@@ -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

View File

@@ -26,6 +26,7 @@ export const mockQueue = (): TQueueServiceFactory => {
getRepeatableJobs: async () => [],
clearQueue: async () => {},
stopJobById: async () => {},
stopJobByIdPg: async () => {},
stopRepeatableJobByJobId: async () => true,
stopRepeatableJobByKey: async () => true
};

View File

@@ -10,8 +10,8 @@ import { TAuditLogServiceFactory, TCreateAuditLogDTO } from "@app/ee/services/au
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-types";
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-types";
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-types";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-types";
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";

View File

@@ -50,6 +50,8 @@ export const initDbConnection = ({
}
: false
},
// https://knexjs.org/guide/#pool
pool: { min: 0, max: 10 },
migrations: {
tableName: "infisical_migrations"
}
@@ -70,7 +72,8 @@ export const initDbConnection = ({
},
migrations: {
tableName: "infisical_migrations"
}
},
pool: { min: 0, max: 10 }
});
});

View File

@@ -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");
}
});
}

View File

@@ -29,7 +29,12 @@ export const SuperAdminSchema = z.object({
adminIdentityIds: z.string().array().nullable().optional(),
encryptedMicrosoftTeamsAppId: zodBuffer.nullable().optional(),
encryptedMicrosoftTeamsClientSecret: zodBuffer.nullable().optional(),
encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional()
encryptedMicrosoftTeamsBotId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionClientId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionClientSecret: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionSlug: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionId: zodBuffer.nullable().optional(),
encryptedGitHubAppConnectionPrivateKey: zodBuffer.nullable().optional()
});
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@@ -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,

View File

@@ -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 };
}
});

View File

@@ -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 };

View File

@@ -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) {

View File

@@ -31,7 +31,7 @@ export type TCreateAccessApprovalRequestDTO = {
export type TListApprovalRequestsDTO = {
projectSlug: string;
authorProjectMembershipId?: string;
authorUserId?: string;
envSlug?: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -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);

View File

@@ -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
};
};

View File

@@ -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` });

View File

@@ -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;
}>;
};

View File

@@ -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)

View File

@@ -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,

View File

@@ -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 }>>;
};

View 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
};
};

View File

@@ -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()
});

View File

@@ -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);
}
}

View File

@@ -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 = {

View File

@@ -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" });
}

View File

@@ -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 ({

View File

@@ -93,6 +93,7 @@ export type TListApprovalsDTO = {
committer?: string;
limit?: number;
offset?: number;
search?: string;
} & TProjectPermission;
export type TSecretApprovalDetailsDTO = {

View File

@@ -11,7 +11,8 @@ export const PgSqlLock = {
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`),
SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`),
CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`)
CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`),
CreateFolder: (envId: string, projectId: string) => pgAdvisoryLockHashText(`create-folder:${envId}-${projectId}`)
} as const;
// all the key prefixes used must be set here to avoid conflict

View File

@@ -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."
}
}
};

View File

@@ -19,3 +19,5 @@ export const getMinExpiresIn = (exp1: string | number, exp2: string | number): s
return ms1 <= ms2 ? exp1 : exp2;
};
export const convertMsToSecond = (time: number) => time / 1000;

View File

@@ -1,3 +1,4 @@
import { TDynamicSecrets } from "@app/db/schemas";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
export type TGenericPermission = {
@@ -84,3 +85,7 @@ export enum QueueWorkerProfile {
Standard = "standard",
SecretScanning = "secret-scanning"
}
export interface TDynamicSecretWithMetadata extends TDynamicSecrets {
metadata: { id: string; key: string; value: string }[];
}

View File

@@ -377,6 +377,7 @@ export type TQueueServiceFactory = {
stopRepeatableJobByKey: <T extends QueueName>(name: T, repeatJobKey: string) => Promise<boolean>;
clearQueue: (name: QueueName) => Promise<void>;
stopJobById: <T extends QueueName>(name: T, jobId: string) => Promise<void | undefined>;
stopJobByIdPg: <T extends QueueName>(name: T, jobId: string) => Promise<void | undefined>;
getRepeatableJobs: (
name: QueueName,
startOffset?: number,
@@ -542,6 +543,10 @@ export const queueServiceFactory = (
return q.removeRepeatableByKey(repeatJobKey);
};
const stopJobByIdPg: TQueueServiceFactory["stopJobByIdPg"] = async (name, jobId) => {
await pgBoss.deleteJob(name, jobId);
};
const stopJobById: TQueueServiceFactory["stopJobById"] = async (name, jobId) => {
const q = queueContainer[name];
const job = await q.getJob(jobId);
@@ -568,6 +573,7 @@ export const queueServiceFactory = (
stopRepeatableJobByKey,
clearQueue,
stopJobById,
stopJobByIdPg,
getRepeatableJobs,
startPg,
queuePg,

View File

@@ -1903,6 +1903,7 @@ export const registerRoutes = async (
await pkiSubscriberQueue.startDailyAutoRenewalJob();
await kmsService.startService();
await microsoftTeamsService.start();
await dynamicSecretQueueService.init();
// inject all services
server.decorate<FastifyZodProvider["services"]>("services", {
@@ -2020,10 +2021,16 @@ export const registerRoutes = async (
if (licenseSyncJob) {
cronJobs.push(licenseSyncJob);
}
const microsoftTeamsSyncJob = await microsoftTeamsService.initializeBackgroundSync();
if (microsoftTeamsSyncJob) {
cronJobs.push(microsoftTeamsSyncJob);
}
const adminIntegrationsSyncJob = await superAdminService.initializeAdminIntegrationConfigSync();
if (adminIntegrationsSyncJob) {
cronJobs.push(adminIntegrationsSyncJob);
}
}
server.decorate<FastifyZodProvider["store"]>("store", {

View File

@@ -37,7 +37,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
encryptedSlackClientSecret: true,
encryptedMicrosoftTeamsAppId: true,
encryptedMicrosoftTeamsClientSecret: true,
encryptedMicrosoftTeamsBotId: true
encryptedMicrosoftTeamsBotId: true,
encryptedGitHubAppConnectionClientId: true,
encryptedGitHubAppConnectionClientSecret: true,
encryptedGitHubAppConnectionSlug: true,
encryptedGitHubAppConnectionId: true,
encryptedGitHubAppConnectionPrivateKey: true
}).extend({
isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(),
@@ -87,6 +92,11 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
microsoftTeamsAppId: z.string().optional(),
microsoftTeamsClientSecret: z.string().optional(),
microsoftTeamsBotId: z.string().optional(),
gitHubAppConnectionClientId: z.string().optional(),
gitHubAppConnectionClientSecret: z.string().optional(),
gitHubAppConnectionSlug: z.string().optional(),
gitHubAppConnectionId: z.string().optional(),
gitHubAppConnectionPrivateKey: z.string().optional(),
authConsentContent: z
.string()
.trim()
@@ -348,6 +358,13 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
appId: z.string(),
clientSecret: z.string(),
botId: z.string()
}),
gitHubAppConnection: z.object({
clientId: z.string(),
clientSecret: z.string(),
appSlug: z.string(),
appId: z.string(),
privateKey: z.string()
})
})
}

View File

@@ -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) => {

View File

@@ -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;
}
});
};

View File

@@ -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
};

View File

@@ -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",

View File

@@ -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({

View File

@@ -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
});

View File

@@ -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
};

View File

@@ -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) => {

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { ApiKeysSchema } from "@app/db/schemas/api-keys";
import { authRateLimit, readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
@@ -13,7 +13,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
url: "/me/emails/code",
config: {
rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) ?? req.realIp
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) || req.realIp
})
},
schema: {
@@ -34,7 +34,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/me/emails/verify",
config: {
rateLimit: authRateLimit
rateLimit: smtpRateLimit({
keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) || req.realIp
})
},
schema: {
body: z.object({

View File

@@ -4,7 +4,7 @@ import { z } from "zod";
import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { secretsLimit, writeLimit } from "@app/server/config/rateLimiter";
import { BaseSecretNameSchema, SecretNameSchema } from "@app/server/lib/schemas";
@@ -12,7 +12,6 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { ProjectFilterType } from "@app/services/project/project-types";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
@@ -286,22 +285,17 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment = scope[0].environment;
workspaceId = req.auth.serviceToken.projectId;
}
} else if (req.permission.type === ActorType.IDENTITY && req.query.workspaceSlug && !workspaceId) {
const workspace = await server.services.project.getAProject({
filter: {
type: ProjectFilterType.SLUG,
orgId: req.permission.orgId,
slug: req.query.workspaceSlug
},
} else {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: req.query.workspaceSlug,
projectId: workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
if (!workspace) throw new NotFoundError({ message: `No project found with slug ${req.query.workspaceSlug}` });
workspaceId = workspace.id;
workspaceId = projectId;
}
if (!workspaceId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" });
@@ -442,11 +436,23 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment = scope[0].environment;
workspaceId = req.auth.serviceToken.projectId;
}
} else {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: workspaceSlug,
projectId: workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
workspaceId = projectId;
}
if (!environment) throw new BadRequestError({ message: "Missing environment" });
if (!workspaceId && !workspaceSlug)
if (!workspaceId) {
throw new BadRequestError({ message: "You must provide workspaceSlug or workspaceId" });
}
const secret = await server.services.secret.getSecretByNameRaw({
actorId: req.permission.id,
@@ -457,7 +463,6 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment,
projectId: workspaceId,
viewSecretValue: req.query.viewSecretValue,
projectSlug: workspaceSlug,
path: secretPath,
secretName: req.params.secretName,
type: req.query.type,
@@ -518,7 +523,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName)
}),
body: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.CREATE.workspaceId),
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.CREATE.workspaceId),
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.CREATE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.CREATE.environment),
secretPath: z
.string()
@@ -558,13 +564,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: req.body.projectSlug,
projectId: req.body.workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
const secretOperation = await server.services.secret.createSecretRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
environment: req.body.environment,
actorAuthMethod: req.permission.authMethod,
projectId: req.body.workspaceId,
projectId,
secretPath: req.body.secretPath,
secretName: req.params.secretName,
type: req.body.type,
@@ -582,7 +597,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({
projectId: req.body.workspaceId,
projectId,
...req.auditLogInfo,
event: {
type: EventType.CREATE_SECRET,
@@ -602,7 +617,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: 1,
workspaceId: req.body.workspaceId,
workspaceId: projectId,
environment: req.body.environment,
secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
@@ -633,7 +648,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName)
}),
body: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.UPDATE.workspaceId),
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.workspaceId),
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.UPDATE.environment),
secretValue: z
.string()
@@ -679,13 +695,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: req.body.projectSlug,
projectId: req.body.workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
const secretOperation = await server.services.secret.updateSecretRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
environment: req.body.environment,
projectId: req.body.workspaceId,
projectId,
secretPath: req.body.secretPath,
secretName: req.params.secretName,
type: req.body.type,
@@ -707,7 +732,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({
projectId: req.body.workspaceId,
projectId,
...req.auditLogInfo,
event: {
type: EventType.UPDATE_SECRET,
@@ -727,7 +752,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: 1,
workspaceId: req.body.workspaceId,
workspaceId: projectId,
environment: req.body.environment,
secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
@@ -757,7 +782,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName)
}),
body: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.DELETE.workspaceId),
workspaceId: z.string().trim().optional().describe(RAW_SECRETS.DELETE.workspaceId),
projectSlug: z.string().trim().optional().describe(RAW_SECRETS.DELETE.projectSlug),
environment: z.string().trim().describe(RAW_SECRETS.DELETE.environment),
secretPath: z
.string()
@@ -780,13 +806,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectId = await server.services.project.extractProjectIdFromSlug({
projectSlug: req.body.projectSlug,
projectId: req.body.workspaceId,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
const secretOperation = await server.services.secret.deleteSecretRaw({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
environment: req.body.environment,
projectId: req.body.workspaceId,
projectId,
secretPath: req.body.secretPath,
secretName: req.params.secretName,
type: req.body.type
@@ -798,7 +833,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({
projectId: req.body.workspaceId,
projectId,
...req.auditLogInfo,
event: {
type: EventType.DELETE_SECRET,
@@ -817,7 +852,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: 1,
workspaceId: req.body.workspaceId,
workspaceId: projectId,
environment: req.body.environment,
secretPath: req.body.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),

View File

@@ -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({

View File

@@ -25,7 +25,8 @@ export enum AppConnection {
OnePass = "1password",
Heroku = "heroku",
Render = "render",
Flyio = "flyio"
Flyio = "flyio",
Cloudflare = "cloudflare"
}
export enum AWSRegion {

View File

@@ -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 (

View File

@@ -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
};

View File

@@ -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)
};
};

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
export enum CloudflareConnectionMethod {
APIToken = "api-token"
}

View File

@@ -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;
};

View File

@@ -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()
});

View File

@@ -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
};
};

View File

@@ -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;
};

View File

@@ -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
{

View File

@@ -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",

View File

@@ -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
};
};

View File

@@ -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;
});

View File

@@ -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
};

View File

@@ -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"
}
}
);
}
};

View File

@@ -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)
});

View File

@@ -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;
};

View File

@@ -18,7 +18,8 @@ export enum SecretSync {
OnePass = "1password",
Heroku = "heroku",
Render = "render",
Flyio = "flyio"
Flyio = "flyio",
CloudflarePages = "cloudflare-pages"
}
export enum SecretSyncInitialSyncBehavior {

View File

@@ -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}`

View File

@@ -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
};

View File

@@ -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;

View File

@@ -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({

View File

@@ -229,8 +229,7 @@ export type TGetASecretRawDTO = {
type: "shared" | "personal";
includeImports?: boolean;
version?: number;
projectSlug?: string;
projectId?: string;
projectId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetASecretByIdRawDTO = {

View File

@@ -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
};
};

View File

@@ -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;
};
};

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/cloudflare/available"
---

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/cloudflare/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/cloudflare/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/cloudflare/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/cloudflare"
---

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/cloudflare-pages"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/cloudflare-pages/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/cloudflare-pages/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/cloudflare-pages/sync-name/{syncName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/cloudflare-pages"
---

View File

@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/cloudflare-pages/{syncId}/remove-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/cloudflare-pages/{syncId}/sync-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/cloudflare-pages/{syncId}"
---

2257
docs/docs.json Normal file

File diff suppressed because it is too large Load Diff

View 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>

View 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**.
![integrations github app create](/images/integrations/github/app/self-hosted-github-app-create.png)
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.
![integrations github app webhook](/images/integrations/github/app/self-hosted-github-app-webhook.png)
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.
![integrations github app create confirm](/images/integrations/github/app/self-hosted-github-app-create-confirm.png)
<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.
![integrations github app create private key](/images/integrations/github/app/self-hosted-github-app-private-key.png)
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.
![Install App](/images/platform/dynamic-secrets/github/install-app.png)
![Install App](/images/platform/dynamic-secrets/github/install-app-modal.png)
Once you've installed the app, **copy the installation ID** from the URL and save it for later steps.
![Install App](/images/platform/dynamic-secrets/github/installation.png)
</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">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select 'GitHub'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/github/modal.png)
</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.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png)
When generating these secrets, the TTL will be fixed to 1 hour.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
![Dynamic Secret Lease](/images/platform/dynamic-secrets/github/lease.png)
</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.
![Lease Data](/images/platform/dynamic-secrets/lease-data.png)
<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>

View File

@@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

Some files were not shown because too many files have changed in this diff Show More