diff --git a/.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml b/.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml index fcf519720..50cb9ecb0 100644 --- a/.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml +++ b/.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml @@ -51,11 +51,18 @@ jobs: --from-literal=ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 \ --from-literal=SITE_URL=http://localhost:8080 + - name: Create bootstrap secret + run: | + kubectl create secret generic infisical-bootstrap-credentials \ + --namespace infisical-standalone-postgres \ + --from-literal=INFISICAL_ADMIN_EMAIL=admin@example.com \ + --from-literal=INFISICAL_ADMIN_PASSWORD=admin-password + - name: Run chart-testing (install) run: | ct install \ --config ct.yaml \ --charts helm-charts/infisical-standalone-postgres \ --helm-extra-args="--timeout=300s" \ - --helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres" \ + --helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres --set infisical.autoBootstrap.enabled=true" \ --namespace infisical-standalone-postgres diff --git a/.infisicalignore b/.infisicalignore index 8cf00ac26..d705c0d66 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -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 diff --git a/backend/e2e-test/mocks/queue.ts b/backend/e2e-test/mocks/queue.ts index 3f49bcfea..58eebdedf 100644 --- a/backend/e2e-test/mocks/queue.ts +++ b/backend/e2e-test/mocks/queue.ts @@ -26,6 +26,7 @@ export const mockQueue = (): TQueueServiceFactory => { getRepeatableJobs: async () => [], clearQueue: async () => {}, stopJobById: async () => {}, + stopJobByIdPg: async () => {}, stopRepeatableJobByJobId: async () => true, stopRepeatableJobByKey: async () => true }; diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 5c05e3f14..4fe4e17cf 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -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"; diff --git a/backend/src/db/instance.ts b/backend/src/db/instance.ts index cdb8c3028..3ce8148aa 100644 --- a/backend/src/db/instance.ts +++ b/backend/src/db/instance.ts @@ -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 } }); }); diff --git a/backend/src/db/migrations/20250620144939_add-instance-github-app-connection-credentials.ts b/backend/src/db/migrations/20250620144939_add-instance-github-app-connection-credentials.ts new file mode 100644 index 000000000..0da41b2e1 --- /dev/null +++ b/backend/src/db/migrations/20250620144939_add-instance-github-app-connection-credentials.ts @@ -0,0 +1,91 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + 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 { + 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"); + } + }); +} diff --git a/backend/src/db/schemas/super-admin.ts b/backend/src/db/schemas/super-admin.ts index ec35042ad..de4975b20 100644 --- a/backend/src/db/schemas/super-admin.ts +++ b/backend/src/db/schemas/super-admin.ts @@ -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; diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 65fcf3c86..8a6b2be88 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -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, diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index ce745245f..e558062b1 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -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 }; } }); diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts index c69c55041..33e9f7a32 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -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 }; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 70d491bf0..5a3af5aa5 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -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) { diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts index fb3e78de0..2550f2a96 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts @@ -31,7 +31,7 @@ export type TCreateAccessApprovalRequestDTO = { export type TListApprovalRequestsDTO = { projectSlug: string; - authorProjectMembershipId?: string; + authorUserId?: string; envSlug?: string; } & Omit; diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts index e9f00f401..525de9efd 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts @@ -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; +export interface TDynamicSecretLeaseDALFactory extends Omit, "findById"> { + countLeasesForDynamicSecret: (dynamicSecretId: string, tx?: Knex) => Promise; + 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); diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts index 497e94311..93c3dd147 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts @@ -21,7 +21,12 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = { folderDAL: Pick; }; -export type TDynamicSecretLeaseQueueServiceFactory = ReturnType; +export type TDynamicSecretLeaseQueueServiceFactory = { + pruneDynamicSecret: (dynamicSecretCfgId: string) => Promise; + setLeaseRevocation: (leaseId: string, expiryAt: Date) => Promise; + unsetLeaseRevocation: (leaseId: string) => Promise; + init: () => Promise; +}; 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( 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( 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 => { 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( + QueueJobs.DynamicSecretRevocation, + async ([job]) => { + await $dynamicSecretQueueJob(job.name, job.id, job.data); + }, + { + workerCount: 5, + pollingIntervalSeconds: 1 + } + ); + + await queueService.startPg( + QueueJobs.DynamicSecretPruning, + async ([job]) => { + await $dynamicSecretQueueJob(job.name, job.id, job.data); + }, + { + workerCount: 1, + pollingIntervalSeconds: 1 + } + ); + }; + return { pruneDynamicSecret, setLeaseRevocation, - unsetLeaseRevocation + unsetLeaseRevocation, + init }; }; diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index 168b16c5f..cf37626c7 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -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; - 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` }); diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-types.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-types.ts index f6d9f6297..c6dfe7b16 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-types.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-types.ts @@ -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; + revokeLease: (arg: TDeleteDynamicSecretLeaseDTO) => Promise; + renewLease: (arg: TRenewDynamicSecretLeaseDTO) => Promise; + 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; + }>; +}; diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts index d7f78c3b1..d5a31614e 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts @@ -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; +export interface TDynamicSecretDALFactory extends Omit, "findOne"> { + findOne: (filter: TFindFilter, tx?: Knex) => Promise; + listDynamicSecretsByFolderIds: ( + arg: { + folderIds: string[]; + search?: string | undefined; + limit?: number | undefined; + offset?: number | undefined; + orderBy?: SecretsOrderBy | undefined; + orderDirection?: OrderByDirection | undefined; + }, + tx?: Knex + ) => Promise>; + findWithMetadata: ( + filter: TFindFilter, + arg?: TFindOpt + ) => Promise; +} -export const dynamicSecretDALFactory = (db: TDbClient) => { +export const dynamicSecretDALFactory = (db: TDbClient): TDynamicSecretDALFactory => { const orm = ormify(db, TableName.DynamicSecret); - const findOne = async (filter: TFindFilter, 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, - { offset, limit, sort, tx }: TFindOpt = {} + 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) diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 5a7da6a3e..d0d14ddaf 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -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; }; -export type TDynamicSecretServiceFactory = ReturnType; - 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, diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts index 6720cf2c8..0e135caef 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-types.ts @@ -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 & { projectId: string; }; + +export type TDynamicSecretServiceFactory = { + create: (arg: TCreateDynamicSecretDTO) => Promise; + updateByName: (arg: TUpdateDynamicSecretDTO) => Promise; + deleteByName: (arg: TDeleteDynamicSecretDTO) => Promise; + getDetails: (arg: TDetailsDynamicSecretDTO) => Promise; + listDynamicSecretsByEnv: (arg: TListDynamicSecretsDTO) => Promise; + listDynamicSecretsByEnvs: ( + arg: TListDynamicSecretsMultiEnvDTO + ) => Promise>; + getDynamicSecretCount: (arg: TGetDynamicSecretsCountDTO) => Promise; + getCountMultiEnv: (arg: TListDynamicSecretsMultiEnvDTO) => Promise; + fetchAzureEntraIdUsers: (arg: { tenantId: string; applicationId: string; clientSecret: string }) => Promise< + { + name: string; + id: string; + email: string; + }[] + >; + listDynamicSecretsByFolderIds: ( + arg: TListDynamicSecretsByFolderMappingsDTO, + actor: OrgServiceActor + ) => Promise>; +}; diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts index f7383d4ac..ec75bb2e4 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts @@ -128,11 +128,21 @@ export const AwsIamProvider = (): TDynamicProviderFns => { const username = generateUsername(usernameTemplate, identity); const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs; + const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }]; + + if (providerInputs.tags && Array.isArray(providerInputs.tags)) { + const additionalTags = providerInputs.tags.map((tag) => ({ + Key: tag.key, + Value: tag.value + })); + awsTags.push(...additionalTags); + } + const createUserRes = await client.send( new CreateUserCommand({ Path: awsPath, PermissionsBoundary: permissionBoundaryPolicyArn || undefined, - Tags: [{ Key: "createdBy", Value: "infisical-dynamic-secret" }], + Tags: awsTags, UserName: username }) ); diff --git a/backend/src/ee/services/dynamic-secret/providers/github.ts b/backend/src/ee/services/dynamic-secret/providers/github.ts new file mode 100644 index 000000000..67d92b2a6 --- /dev/null +++ b/backend/src/ee/services/dynamic-secret/providers/github.ts @@ -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; + 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 => { + 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(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 + }; +}; diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index 7e14cf1ab..7fd65f98d 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -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() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts index 45cc06e7c..afc4804b6 100644 --- a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts +++ b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts @@ -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 ): Promise => { @@ -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); } } diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 8f361e166..f6fa2a4a8 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -2,6 +2,7 @@ import RE2 from "re2"; import { z } from "zod"; import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; +import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { TDynamicSecretLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types"; @@ -207,7 +208,8 @@ export const DynamicSecretAwsIamSchema = z.preprocess( permissionBoundaryPolicyArn: z.string().trim().optional(), policyDocument: z.string().trim().optional(), userGroups: z.string().trim().optional(), - policyArns: z.string().trim().optional() + policyArns: z.string().trim().optional(), + tags: ResourceMetadataSchema.optional() }), z.object({ method: z.literal(AwsIamAuthType.AssumeRole), @@ -217,7 +219,8 @@ export const DynamicSecretAwsIamSchema = z.preprocess( permissionBoundaryPolicyArn: z.string().trim().optional(), policyDocument: z.string().trim().optional(), userGroups: z.string().trim().optional(), - policyArns: z.string().trim().optional() + policyArns: z.string().trim().optional(), + tags: ResourceMetadataSchema.optional() }) ]) ); @@ -474,6 +477,23 @@ export const DynamicSecretGcpIamSchema = z.object({ serviceAccountEmail: z.string().email().trim().min(1, "Service account email required").max(128) }); +export const DynamicSecretGithubSchema = z.object({ + appId: z.number().min(1).describe("The ID of your GitHub App."), + installationId: z.number().min(1).describe("The ID of the GitHub App installation."), + privateKey: z + .string() + .trim() + .min(1) + .refine( + (val) => + new RE2( + /^-----BEGIN(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----\s*[\s\S]*?-----END(?:(?: RSA| PGP| ENCRYPTED)? PRIVATE KEY)-----$/ + ).test(val), + "Invalid PEM format for private key" + ) + .describe("The private key generated for your GitHub App.") +}); + export enum DynamicSecretProviders { SqlDatabase = "sql-database", Cassandra = "cassandra", @@ -492,7 +512,8 @@ export enum DynamicSecretProviders { SapAse = "sap-ase", Kubernetes = "kubernetes", Vertica = "vertica", - GcpIam = "gcp-iam" + GcpIam = "gcp-iam", + Github = "github" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ @@ -513,7 +534,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }), z.object({ type: z.literal(DynamicSecretProviders.Kubernetes), inputs: DynamicSecretKubernetesSchema }), z.object({ type: z.literal(DynamicSecretProviders.Vertica), inputs: DynamicSecretVerticaSchema }), - z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema }) + z.object({ type: z.literal(DynamicSecretProviders.GcpIam), inputs: DynamicSecretGcpIamSchema }), + z.object({ type: z.literal(DynamicSecretProviders.Github), inputs: DynamicSecretGithubSchema }) ]); export type TDynamicProviderFns = { diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index 3bd35c3c8..5e1e546d6 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -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[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[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" }); } diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index e70d0af00..49f336111 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -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 ({ diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts index 839833a9c..2fdb0bb9d 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts @@ -93,6 +93,7 @@ export type TListApprovalsDTO = { committer?: string; limit?: number; offset?: number; + search?: string; } & TProjectPermission; export type TSecretApprovalDetailsDTO = { diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 6a63af776..1e641e813 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -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 diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 5d4a91eec..be460b4b4 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2418,6 +2418,10 @@ export const SecretSyncs = { shouldProtectSecrets: "Whether variables should be protected", shouldMaskSecrets: "Whether variables should be masked in logs", shouldHideSecrets: "Whether variables should be hidden" + }, + CLOUDFLARE_PAGES: { + projectName: "The name of the Cloudflare Pages project to sync secrets to.", + environment: "The environment of the Cloudflare Pages project to sync secrets to." } } }; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 9166e7569..4fb19e7bb 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -101,9 +101,9 @@ const envSchema = z LOOPS_API_KEY: zpStr(z.string().optional()), // jwt options AUTH_SECRET: zpStr(z.string()).default(process.env.JWT_AUTH_SECRET), // for those still using old JWT_AUTH_SECRET - JWT_AUTH_LIFETIME: zpStr(z.string().default("1d")), + JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")), JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")), - JWT_REFRESH_LIFETIME: zpStr(z.string().default("14d")), + JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")), JWT_INVITE_LIFETIME: zpStr(z.string().default("1d")), JWT_MFA_LIFETIME: zpStr(z.string().default("5m")), JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")), diff --git a/backend/src/lib/fn/time.ts b/backend/src/lib/fn/time.ts index 27bd8f8a6..7276949c7 100644 --- a/backend/src/lib/fn/time.ts +++ b/backend/src/lib/fn/time.ts @@ -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; diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts index 49d8893be..a7a60349f 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -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 }[]; +} diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 16c5bb38f..b3be02c72 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -377,6 +377,7 @@ export type TQueueServiceFactory = { stopRepeatableJobByKey: (name: T, repeatJobKey: string) => Promise; clearQueue: (name: QueueName) => Promise; stopJobById: (name: T, jobId: string) => Promise; + stopJobByIdPg: (name: T, jobId: string) => Promise; 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, diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 211bcea0c..f065bfbed 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -107,7 +107,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { server.addHook("onRequest", async (req) => { const appCfg = getConfig(); - if (req.url.includes(".well-known/est") || req.url.includes("/api/v3/auth/") || req.url === "/api/v1/auth/token") { + if (req.url.includes(".well-known/est") || req.url.includes("/api/v3/auth/")) { return; } diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 262e8f373..bde1b805e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -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("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("store", { diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 0bade9904..f01f1722c 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -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() }) }) } diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 1f3757073..6160828f4 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -35,6 +35,10 @@ import { CamundaConnectionListItemSchema, SanitizedCamundaConnectionSchema } from "@app/services/app-connection/camunda"; +import { + CloudflareConnectionListItemSchema, + SanitizedCloudflareConnectionSchema +} from "@app/services/app-connection/cloudflare/cloudflare-connection-schema"; import { DatabricksConnectionListItemSchema, SanitizedDatabricksConnectionSchema @@ -111,7 +115,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedHerokuConnectionSchema.options, ...SanitizedRenderConnectionSchema.options, ...SanitizedFlyioConnectionSchema.options, - ...SanitizedGitLabConnectionSchema.options + ...SanitizedGitLabConnectionSchema.options, + ...SanitizedCloudflareConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -142,7 +147,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ HerokuConnectionListItemSchema, RenderConnectionListItemSchema, FlyioConnectionListItemSchema, - GitLabConnectionListItemSchema + GitLabConnectionListItemSchema, + CloudflareConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/cloudflare-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/cloudflare-connection-router.ts new file mode 100644 index 000000000..bd3507a7d --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/cloudflare-connection-router.ts @@ -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; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index b1a385fd7..cd4ccd728 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -10,6 +10,7 @@ import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secre import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router"; import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; import { registerCamundaConnectionRouter } from "./camunda-connection-router"; +import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerFlyioConnectionRouter } from "./flyio-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; @@ -60,5 +61,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { 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", diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts index eeb730f29..3396cebe9 100644 --- a/backend/src/server/routes/v1/password-router.ts +++ b/backend/src/server/routes/v1/password-router.ts @@ -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({ diff --git a/backend/src/server/routes/v1/secret-sync-routers/cloudflare-pages-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/cloudflare-pages-sync-router.ts new file mode 100644 index 000000000..6a70b5837 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/cloudflare-pages-sync-router.ts @@ -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 + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index dff5937c2..4675a1a40 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -8,6 +8,7 @@ import { registerAzureAppConfigurationSyncRouter } from "./azure-app-configurati import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router"; import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; import { registerCamundaSyncRouter } from "./camunda-sync-router"; +import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerFlyioSyncRouter } from "./flyio-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; @@ -45,5 +46,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts index bbd566334..92b4138f2 100644 --- a/backend/src/server/routes/v2/user-router.ts +++ b/backend/src/server/routes/v2/user-router.ts @@ -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({ diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index c7740b665..91df68e16 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -50,8 +50,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { 200: z.object({ token: z.string(), isMfaEnabled: z.boolean(), - mfaMethod: z.string().optional(), - refreshToken: z.string().optional() + mfaMethod: z.string().optional() }) } }, @@ -102,7 +101,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { maxAge: 0 }); - return { token: tokens.access, isMfaEnabled: false, refreshToken: tokens.refresh }; + return { token: tokens.access, isMfaEnabled: false }; } }); @@ -130,8 +129,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { encryptedPrivateKey: z.string(), iv: z.string(), tag: z.string(), - token: z.string(), - refreshToken: z.string().optional() + token: z.string() }) } }, @@ -174,8 +172,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { tag: data.user.tag, protectedKey: data.user.protectedKey || null, protectedKeyIV: data.user.protectedKeyIV || null, - protectedKeyTag: data.user.protectedKeyTag || null, - refreshToken: data.token.refresh + protectedKeyTag: data.user.protectedKeyTag || null } as const; } }); diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index c986e40b4..33878f1e7 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -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"]), diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index c249e7dbe..393b598cf 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -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({ diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index e55dfa3b1..11b84b5ad 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -26,7 +26,8 @@ export enum AppConnection { Heroku = "heroku", Render = "render", Flyio = "flyio", - GitLab = "gitlab" + GitLab = "gitlab", + Cloudflare = "cloudflare" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 2c46d5fc3..78f6b99b5 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -51,6 +51,11 @@ import { validateAzureKeyVaultConnectionCredentials } from "./azure-key-vault"; import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda"; +import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum"; +import { + getCloudflareConnectionListItem, + validateCloudflareConnectionCredentials +} from "./cloudflare/cloudflare-connection-fns"; import { DatabricksConnectionMethod, getDatabricksConnectionListItem, @@ -130,7 +135,8 @@ export const listAppConnectionOptions = () => { getHerokuConnectionListItem(), getRenderConnectionListItem(), getFlyioConnectionListItem(), - getGitLabConnectionListItem() + getGitLabConnectionListItem(), + getCloudflareConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -209,7 +215,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Heroku]: validateHerokuConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); @@ -245,6 +252,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case TerraformCloudConnectionMethod.ApiToken: case VercelConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken: + case CloudflareConnectionMethod.APIToken: return "API Token"; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: @@ -323,7 +331,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Heroku]: platformManagedCredentialsNotSupported, [AppConnection.Render]: platformManagedCredentialsNotSupported, [AppConnection.Flyio]: platformManagedCredentialsNotSupported, - [AppConnection.GitLab]: platformManagedCredentialsNotSupported + [AppConnection.GitLab]: platformManagedCredentialsNotSupported, + [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported }; export const enterpriseAppCheck = async ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index d605279a3..9c0a3b5b8 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -28,7 +28,8 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Heroku]: "Heroku", [AppConnection.Render]: "Render", [AppConnection.Flyio]: "Fly.io", - [AppConnection.GitLab]: "GitLab" + [AppConnection.GitLab]: "GitLab", + [AppConnection.Cloudflare]: "Cloudflare" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -59,5 +60,6 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -224,6 +231,7 @@ export type TAppConnectionInput = { id: string } & ( | TRenderConnectionInput | TFlyioConnectionInput | TGitLabConnectionInput + | TCloudflareConnectionInput ); export type TSqlConnectionInput = @@ -266,7 +274,8 @@ export type TAppConnectionConfig = | THerokuConnectionConfig | TRenderConnectionConfig | TFlyioConnectionConfig - | TGitLabConnectionConfig; + | TGitLabConnectionConfig + | TCloudflareConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -296,7 +305,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateHerokuConnectionCredentialsSchema | TValidateRenderConnectionCredentialsSchema | TValidateFlyioConnectionCredentialsSchema - | TValidateGitLabConnectionCredentialsSchema; + | TValidateGitLabConnectionCredentialsSchema + | TValidateCloudflareConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/cloudflare/cloudflare-connection-enum.ts b/backend/src/services/app-connection/cloudflare/cloudflare-connection-enum.ts new file mode 100644 index 000000000..2381524b9 --- /dev/null +++ b/backend/src/services/app-connection/cloudflare/cloudflare-connection-enum.ts @@ -0,0 +1,3 @@ +export enum CloudflareConnectionMethod { + APIToken = "api-token" +} diff --git a/backend/src/services/app-connection/cloudflare/cloudflare-connection-fns.ts b/backend/src/services/app-connection/cloudflare/cloudflare-connection-fns.ts new file mode 100644 index 000000000..28ad44de0 --- /dev/null +++ b/backend/src/services/app-connection/cloudflare/cloudflare-connection-fns.ts @@ -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 => { + 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; +}; diff --git a/backend/src/services/app-connection/cloudflare/cloudflare-connection-schema.ts b/backend/src/services/app-connection/cloudflare/cloudflare-connection-schema.ts new file mode 100644 index 000000000..64dee9dd5 --- /dev/null +++ b/backend/src/services/app-connection/cloudflare/cloudflare-connection-schema.ts @@ -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() +}); diff --git a/backend/src/services/app-connection/cloudflare/cloudflare-connection-service.ts b/backend/src/services/app-connection/cloudflare/cloudflare-connection-service.ts new file mode 100644 index 000000000..2d1f38786 --- /dev/null +++ b/backend/src/services/app-connection/cloudflare/cloudflare-connection-service.ts @@ -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; + +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 + }; +}; diff --git a/backend/src/services/app-connection/cloudflare/cloudflare-connection-types.ts b/backend/src/services/app-connection/cloudflare/cloudflare-connection-types.ts new file mode 100644 index 000000000..6b2ee0d04 --- /dev/null +++ b/backend/src/services/app-connection/cloudflare/cloudflare-connection-types.ts @@ -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; + +export type TCloudflareConnectionInput = z.infer & { + 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; +}; diff --git a/backend/src/services/app-connection/github/github-connection-fns.ts b/backend/src/services/app-connection/github/github-connection-fns.ts index 6ec675c0f..ebdd09289 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -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 { diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index e8ef72f7a..9b9841f1f 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -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", diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 4650c474d..29774fcc8 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -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 }; }; diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index d6007957b..da29c0f36 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -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 = 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; }); diff --git a/backend/src/services/secret-sync/cloudflare-pages/cloudflare-pages-constants.ts b/backend/src/services/secret-sync/cloudflare-pages/cloudflare-pages-constants.ts new file mode 100644 index 000000000..4ec02074c --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-pages/cloudflare-pages-constants.ts @@ -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 +}; diff --git a/backend/src/services/secret-sync/cloudflare-pages/cloudflare-pages-fns.ts b/backend/src/services/secret-sync/cloudflare-pages/cloudflare-pages-fns.ts new file mode 100644 index 000000000..e2754da7a --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-pages/cloudflare-pages-fns.ts @@ -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; + } + >; + }; + }>( + `${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 => { + 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" + } + } + ); + } +}; diff --git a/backend/src/services/secret-sync/cloudflare-pages/cloudflare-pages-schema.ts b/backend/src/services/secret-sync/cloudflare-pages/cloudflare-pages-schema.ts new file mode 100644 index 000000000..f0d814fb6 --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-pages/cloudflare-pages-schema.ts @@ -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) +}); diff --git a/backend/src/services/secret-sync/cloudflare-pages/cloudflare-pages-types.ts b/backend/src/services/secret-sync/cloudflare-pages/cloudflare-pages-types.ts new file mode 100644 index 000000000..e87280206 --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-pages/cloudflare-pages-types.ts @@ -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; + +export type TCloudflarePagesSync = z.infer; + +export type TCloudflarePagesSyncInput = z.infer; + +export type TCloudflarePagesSyncWithCredentials = TCloudflarePagesSync & { + connection: TCloudflareConnection; +}; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 8fededef2..b70b37caf 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -19,7 +19,8 @@ export enum SecretSync { Heroku = "heroku", Render = "render", Flyio = "flyio", - GitLab = "gitlab" + GitLab = "gitlab", + CloudflarePages = "cloudflare-pages" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 7d7fab0b0..9d0513a2c 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -29,6 +29,8 @@ import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFact import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops"; import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault"; import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; +import { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants"; +import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns"; import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; @@ -65,7 +67,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.Heroku]: HEROKU_SYNC_LIST_OPTION, [SecretSync.Render]: RENDER_SYNC_LIST_OPTION, [SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION, - [SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION + [SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION, + [SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -231,6 +234,8 @@ export const SecretSyncFns = { return FlyioSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.GitLab: return GitLabSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); + case SecretSync.CloudflarePages: + return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -320,6 +325,9 @@ export const SecretSyncFns = { case SecretSync.GitLab: secretMap = await GitLabSyncFns.getSecrets(secretSync); break; + case SecretSync.CloudflarePages: + secretMap = await CloudflarePagesSyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -395,6 +403,8 @@ export const SecretSyncFns = { return FlyioSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.GitLab: return GitLabSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); + case SecretSync.CloudflarePages: + return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 6a29b71da..1dc0ea6c0 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -22,7 +22,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Heroku]: "Heroku", [SecretSync.Render]: "Render", [SecretSync.Flyio]: "Fly.io", - [SecretSync.GitLab]: "GitLab" + [SecretSync.GitLab]: "GitLab", + [SecretSync.CloudflarePages]: "Cloudflare Pages" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -46,7 +47,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.Heroku]: AppConnection.Heroku, [SecretSync.Render]: AppConnection.Render, [SecretSync.Flyio]: AppConnection.Flyio, - [SecretSync.GitLab]: AppConnection.GitLab + [SecretSync.GitLab]: AppConnection.GitLab, + [SecretSync.CloudflarePages]: AppConnection.Cloudflare }; export const SECRET_SYNC_PLAN_MAP: Record = { @@ -70,5 +72,6 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.Heroku]: SecretSyncPlanType.Regular, [SecretSync.Render]: SecretSyncPlanType.Regular, [SecretSync.Flyio]: SecretSyncPlanType.Regular, - [SecretSync.GitLab]: SecretSyncPlanType.Regular + [SecretSync.GitLab]: SecretSyncPlanType.Regular, + [SecretSync.CloudflarePages]: SecretSyncPlanType.Regular }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index ef0233d6c..a31183280 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -72,6 +72,12 @@ import { TAzureKeyVaultSyncListItem, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault"; +import { + TCloudflarePagesSync, + TCloudflarePagesSyncInput, + TCloudflarePagesSyncListItem, + TCloudflarePagesSyncWithCredentials +} from "./cloudflare-pages/cloudflare-pages-types"; import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; import { TGitLabSync, TGitLabSyncInput, TGitLabSyncListItem, TGitLabSyncWithCredentials } from "./gitlab"; @@ -129,7 +135,8 @@ export type TSecretSync = | THerokuSync | TRenderSync | TFlyioSync - | TGitLabSync; + | TGitLabSync + | TCloudflarePagesSync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -152,7 +159,8 @@ export type TSecretSyncWithCredentials = | THerokuSyncWithCredentials | TRenderSyncWithCredentials | TFlyioSyncWithCredentials - | TGitLabSyncWithCredentials; + | TGitLabSyncWithCredentials + | TCloudflarePagesSyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -175,7 +183,8 @@ export type TSecretSyncInput = | THerokuSyncInput | TRenderSyncInput | TFlyioSyncInput - | TGitLabSyncInput; + | TGitLabSyncInput + | TCloudflarePagesSyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -198,7 +207,8 @@ export type TSecretSyncListItem = | THerokuSyncListItem | TRenderSyncListItem | TFlyioSyncListItem - | TGitLabSyncListItem; + | TGitLabSyncListItem + | TCloudflarePagesSyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index d5c836ff7..159bee8e3 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -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({ diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 91fc2eb6a..12b8e7175 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -229,8 +229,7 @@ export type TGetASecretRawDTO = { type: "shared" | "personal"; includeImports?: boolean; version?: number; - projectSlug?: string; - projectId?: string; + projectId: string; } & Omit; export type TGetASecretByIdRawDTO = { diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index ff319796e..8a0d4dd14 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -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 }; }; diff --git a/backend/src/services/super-admin/super-admin-types.ts b/backend/src/services/super-admin/super-admin-types.ts index 22803a650..205c59f2c 100644 --- a/backend/src/services/super-admin/super-admin-types.ts +++ b/backend/src/services/super-admin/super-admin-types.ts @@ -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; + }; +}; diff --git a/cli/go.mod b/cli/go.mod index e6d55eb49..3afc8d3be 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -40,6 +40,9 @@ require ( golang.org/x/term v0.30.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.31.4 + k8s.io/apimachinery v0.31.4 + k8s.io/client-go v0.31.4 ) require ( @@ -70,16 +73,25 @@ require ( github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dvsekhvalnov/jose2go v1.6.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/errors v0.20.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/strfmt v0.21.3 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/uuid v1.6.0 // indirect @@ -90,17 +102,23 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.5 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/muesli/mango v0.1.0 // indirect github.com/muesli/mango-pflag v0.1.0 // indirect github.com/muesli/termenv v0.15.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/onsi/ginkgo/v2 v2.22.2 // indirect github.com/pelletier/go-toml v1.9.3 // indirect @@ -117,6 +135,7 @@ require ( github.com/tetratelabs/wazero v1.9.0 // indirect github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect github.com/wlynxg/anet v0.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect go.mongodb.org/mongo-driver v1.10.0 // indirect go.opencensus.io v0.24.0 // indirect @@ -127,18 +146,26 @@ require ( go.opentelemetry.io/otel/trace v1.24.0 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/mod v0.23.0 // indirect - golang.org/x/net v0.35.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.6.0 // indirect + golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.30.0 // indirect google.golang.org/api v0.188.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect google.golang.org/grpc v1.64.1 // indirect - google.golang.org/protobuf v1.36.1 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.62.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) require ( diff --git a/cli/go.sum b/cli/go.sum index 2e41c756b..066f736a2 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -134,6 +134,8 @@ github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMS github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -152,6 +154,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gitleaks/go-gitdiff v0.9.1 h1:ni6z6/3i9ODT685OLCTf+s/ERlWUNWQF4x1pvoNICw0= github.com/gitleaks/go-gitdiff v0.9.1/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF1uXV9ZyNvrA= @@ -165,8 +169,16 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8= github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -174,6 +186,7 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -211,6 +224,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -222,9 +237,12 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -298,7 +316,11 @@ github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BA github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -308,6 +330,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -318,6 +341,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -346,8 +371,12 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= @@ -365,7 +394,8 @@ github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -406,8 +436,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -467,6 +497,8 @@ github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8S github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= @@ -596,8 +628,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -610,8 +642,8 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -693,8 +725,8 @@ golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -863,14 +895,17 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -890,6 +925,27 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.31.4 h1:I2QNzitPVsPeLQvexMEsj945QumYraqv9m74isPDKhM= +k8s.io/api v0.31.4/go.mod h1:d+7vgXLvmcdT1BCo79VEgJxHHryww3V5np2OYTr6jdw= +k8s.io/apimachinery v0.31.4 h1:8xjE2C4CzhYVm9DGf60yohpNUh5AEBnPxCryPBECmlM= +k8s.io/apimachinery v0.31.4/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.4 h1:t4QEXt4jgHIkKKlx06+W3+1JOwAFU/2OPiOo7H92eRQ= +k8s.io/client-go v0.31.4/go.mod h1:kvuMro4sFYIa8sulL5Gi5GFqUPvfH2O/dXuKstbaaeg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 83732b64a..15f75a57d 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -631,8 +631,8 @@ func CallGatewayHeartBeatV1(httpClient *resty.Client) error { return nil } -func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (map[string]interface{}, error) { - var resBody map[string]interface{} +func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (BootstrapInstanceResponse, error) { + var resBody BootstrapInstanceResponse response, err := httpClient. R(). SetResult(&resBody). @@ -641,11 +641,11 @@ func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRe Post(fmt.Sprintf("%v/v1/admin/bootstrap", request.Domain)) if err != nil { - return nil, NewGenericRequestError(operationCallBootstrapInstance, err) + return BootstrapInstanceResponse{}, NewGenericRequestError(operationCallBootstrapInstance, err) } if response.IsError() { - return nil, NewAPIErrorWithResponse(operationCallBootstrapInstance, response, nil) + return BootstrapInstanceResponse{}, NewAPIErrorWithResponse(operationCallBootstrapInstance, response, nil) } return resBody, nil diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 59abe0253..9bf666e44 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -21,7 +21,7 @@ type LoginTwoRequest struct { } type LoginTwoResponse struct { - JWTToken string `json:"token"` + JTWToken string `json:"token"` RefreshToken string `json:"refreshToken"` PublicKey string `json:"publicKey"` EncryptedPrivateKey string `json:"encryptedPrivateKey"` @@ -267,7 +267,7 @@ type GetLoginTwoV2Response struct { ProtectedKey string `json:"protectedKey"` ProtectedKeyIV string `json:"protectedKeyIV"` ProtectedKeyTag string `json:"protectedKeyTag"` - RefreshToken string `json:"refreshToken"` + RefreshToken string `json:"RefreshToken"` } type VerifyMfaTokenRequest struct { @@ -655,3 +655,35 @@ type BootstrapInstanceRequest struct { Organization string `json:"organization"` Domain string `json:"domain"` } + +type BootstrapInstanceResponse struct { + Message string `json:"message"` + Identity BootstrapIdentity `json:"identity"` + Organization BootstrapOrganization `json:"organization"` + User BootstrapUser `json:"user"` +} + +type BootstrapIdentity struct { + ID string `json:"id"` + Name string `json:"name"` + Credentials BootstrapIdentityCredentials `json:"credentials"` +} + +type BootstrapIdentityCredentials struct { + Token string `json:"token"` +} + +type BootstrapOrganization struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` +} + +type BootstrapUser struct { + ID string `json:"id"` + Email string `json:"email"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Username string `json:"username"` + SuperAdmin bool `json:"superAdmin"` +} diff --git a/cli/packages/cmd/bootstrap.go b/cli/packages/cmd/bootstrap.go index 4582cb001..7132b634d 100644 --- a/cli/packages/cmd/bootstrap.go +++ b/cli/packages/cmd/bootstrap.go @@ -4,16 +4,127 @@ Copyright (c) 2023 Infisical Inc. package cmd import ( + "bytes" + "context" + "encoding/base64" "encoding/json" "fmt" "os" + "text/template" "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/util" "github.com/rs/zerolog/log" "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) +// handleK8SecretOutput processes the k8-secret output type by creating a Kubernetes secret +func handleK8SecretOutput(bootstrapResponse api.BootstrapInstanceResponse, k8SecretTemplate, k8SecretName, k8SecretNamespace string) error { + // Create in-cluster config + config, err := rest.InClusterConfig() + if err != nil { + return fmt.Errorf("failed to create in-cluster config: %v", err) + } + + // Create Kubernetes client + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create Kubernetes client: %v", err) + } + + // Parse and execute the template to render the data/stringData section + tmpl, err := template.New("k8-secret-template").Funcs(template.FuncMap{ + "encodeBase64": func(s string) string { + return base64.StdEncoding.EncodeToString([]byte(s)) + }, + }).Parse(k8SecretTemplate) + + if err != nil { + return fmt.Errorf("failed to parse output template: %v", err) + } + + var renderedDataSection bytes.Buffer + err = tmpl.Execute(&renderedDataSection, bootstrapResponse) + if err != nil { + return fmt.Errorf("failed to execute output template: %v", err) + } + + // Parse the rendered template as JSON to validate it's valid + var dataSection map[string]interface{} + if err := json.Unmarshal(renderedDataSection.Bytes(), &dataSection); err != nil { + return fmt.Errorf("template output is not valid JSON: %v", err) + } + + // Prepare the secret data and stringData maps + secretData := make(map[string][]byte) + secretStringData := make(map[string]string) + + // Process the dataSection to separate data and stringData + if data, exists := dataSection["data"]; exists { + if dataMap, ok := data.(map[string]interface{}); ok { + for key, value := range dataMap { + if strValue, ok := value.(string); ok { + secretData[key] = []byte(strValue) + } + } + } + } + + if stringData, exists := dataSection["stringData"]; exists { + if stringDataMap, ok := stringData.(map[string]interface{}); ok { + for key, value := range stringDataMap { + if strValue, ok := value.(string); ok { + secretStringData[key] = strValue + } + } + } + } + + // Create the Kubernetes secret object + k8sSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: k8SecretName, + Namespace: k8SecretNamespace, + }, + Type: corev1.SecretTypeOpaque, + Data: secretData, + StringData: secretStringData, + } + + ctx := context.Background() + secretsClient := clientset.CoreV1().Secrets(k8SecretNamespace) + + // Check if secret already exists + existingSecret, err := secretsClient.Get(ctx, k8SecretName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + // Secret doesn't exist, create it + _, err = secretsClient.Create(ctx, k8sSecret, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create Kubernetes secret: %v", err) + } + log.Info().Msgf("Successfully created Kubernetes secret '%s' in namespace '%s'", k8SecretName, k8SecretNamespace) + } else { + return fmt.Errorf("failed to check if Kubernetes secret exists: %v", err) + } + } else { + // Secret exists, update it + k8sSecret.ObjectMeta.ResourceVersion = existingSecret.ObjectMeta.ResourceVersion + _, err = secretsClient.Update(ctx, k8sSecret, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update Kubernetes secret: %v", err) + } + log.Info().Msgf("Successfully updated Kubernetes secret '%s' in namespace '%s'", k8SecretName, k8SecretNamespace) + } + + return nil +} + var bootstrapCmd = &cobra.Command{ Use: "bootstrap", Short: "Used to bootstrap your Infisical instance", @@ -23,7 +134,7 @@ var bootstrapCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { email, _ := cmd.Flags().GetString("email") if email == "" { - if envEmail, ok := os.LookupEnv("INFISICAL_ADMIN_EMAIL"); ok { + if envEmail, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_EMAIL_NAME); ok { email = envEmail } } @@ -35,7 +146,7 @@ var bootstrapCmd = &cobra.Command{ password, _ := cmd.Flags().GetString("password") if password == "" { - if envPassword, ok := os.LookupEnv("INFISICAL_ADMIN_PASSWORD"); ok { + if envPassword, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_PASSWORD_NAME); ok { password = envPassword } } @@ -47,7 +158,7 @@ var bootstrapCmd = &cobra.Command{ organization, _ := cmd.Flags().GetString("organization") if organization == "" { - if envOrganization, ok := os.LookupEnv("INFISICAL_ADMIN_ORGANIZATION"); ok { + if envOrganization, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_ORGANIZATION_NAME); ok { organization = envOrganization } } @@ -69,11 +180,56 @@ var bootstrapCmd = &cobra.Command{ return } + outputType, err := cmd.Flags().GetString("output") + if err != nil { + log.Error().Msgf("Failed to get output type: %v", err) + return + } + + k8SecretTemplate, err := cmd.Flags().GetString("k8-secret-template") + if err != nil { + log.Error().Msgf("Failed to get k8-secret-template: %v", err) + } + + k8SecretName, err := cmd.Flags().GetString("k8-secret-name") + if err != nil { + log.Error().Msgf("Failed to get k8-secret-name: %v", err) + } + + k8SecretNamespace, err := cmd.Flags().GetString("k8-secret-namespace") + if err != nil { + log.Error().Msgf("Failed to get k8-secret-namespace: %v", err) + } + + if outputType == "k8-secret" { + if k8SecretTemplate == "" { + log.Error().Msg("k8-secret-template is required when using k8-secret output type") + return + } + + if k8SecretName == "" { + log.Error().Msg("k8-secret-name is required when using k8-secret output type") + return + } + + if k8SecretNamespace == "" { + log.Error().Msg("k8-secret-namespace is required when using k8-secret output type") + return + } + } + httpClient, err := util.GetRestyClientWithCustomHeaders() if err != nil { log.Error().Msgf("Failed to get resty client with custom headers: %v", err) return } + + ignoreIfBootstrapped, err := cmd.Flags().GetBool("ignore-if-bootstrapped") + if err != nil { + log.Error().Msgf("Failed to get ignore-if-bootstrapped flag: %v", err) + return + } + httpClient.SetHeader("Accept", "application/json") bootstrapResponse, err := api.CallBootstrapInstance(httpClient, api.BootstrapInstanceRequest{ @@ -84,16 +240,26 @@ var bootstrapCmd = &cobra.Command{ }) if err != nil { - log.Error().Msgf("Failed to bootstrap instance: %v", err) + if !ignoreIfBootstrapped { + log.Error().Msgf("Failed to bootstrap instance: %v", err) + } return } - responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ") - if err != nil { - log.Fatal().Msgf("Failed to convert response to JSON: %v", err) - return + if outputType == "k8-secret" { + if err := handleK8SecretOutput(bootstrapResponse, k8SecretTemplate, k8SecretName, k8SecretNamespace); err != nil { + log.Error().Msgf("Failed to handle k8-secret output: %v", err) + return + } + } else { + responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ") + if err != nil { + log.Fatal().Msgf("Failed to convert response to JSON: %v", err) + return + } + + fmt.Println(string(responseJSON)) } - fmt.Println(string(responseJSON)) }, } @@ -102,6 +268,10 @@ func init() { bootstrapCmd.Flags().String("email", "", "The desired email address of the instance admin") bootstrapCmd.Flags().String("password", "", "The desired password of the instance admin") bootstrapCmd.Flags().String("organization", "", "The name of the organization to create for the instance") - + bootstrapCmd.Flags().String("output", "", "The type of output to use for the bootstrap command (json or k8-secret)") + bootstrapCmd.Flags().Bool("ignore-if-bootstrapped", false, "Whether to continue on error if the instance has already been bootstrapped") + bootstrapCmd.Flags().String("k8-secret-template", "{\"data\":{\"token\":\"{{.Identity.Credentials.Token}}\"}}", "The template to use for rendering the Kubernetes secret (entire secret JSON)") + bootstrapCmd.Flags().String("k8-secret-namespace", "", "The namespace to create the Kubernetes secret in") + bootstrapCmd.Flags().String("k8-secret-name", "", "The name of the Kubernetes secret to create") rootCmd.AddCommand(bootstrapCmd) } diff --git a/cli/packages/cmd/dynamic_secrets.go b/cli/packages/cmd/dynamic_secrets.go index f8a43bc06..0443e7714 100644 --- a/cli/packages/cmd/dynamic_secrets.go +++ b/cli/packages/cmd/dynamic_secrets.go @@ -87,7 +87,7 @@ func getDynamicSecretList(cmd *cobra.Command, args []string) { loggedInUserDetails = util.EstablishUserLoginSession() } - infisicalToken = loggedInUserDetails.UserCredentials.JWTToken + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken } httpClient.SetAuthToken(infisicalToken) @@ -211,7 +211,7 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) { if loggedInUserDetails.LoginExpired { loggedInUserDetails = util.EstablishUserLoginSession() } - infisicalToken = loggedInUserDetails.UserCredentials.JWTToken + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken } httpClient.SetAuthToken(infisicalToken) @@ -363,7 +363,7 @@ func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) { loggedInUserDetails = util.EstablishUserLoginSession() } - infisicalToken = loggedInUserDetails.UserCredentials.JWTToken + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken } httpClient.SetAuthToken(infisicalToken) @@ -478,7 +478,7 @@ func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) { loggedInUserDetails = util.EstablishUserLoginSession() } - infisicalToken = loggedInUserDetails.UserCredentials.JWTToken + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken } httpClient.SetAuthToken(infisicalToken) @@ -592,7 +592,7 @@ func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) { if loggedInUserDetails.LoginExpired { loggedInUserDetails = util.EstablishUserLoginSession() } - infisicalToken = loggedInUserDetails.UserCredentials.JWTToken + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken } httpClient.SetAuthToken(infisicalToken) diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go index 9066b7145..b872b0e61 100644 --- a/cli/packages/cmd/export.go +++ b/cli/packages/cmd/export.go @@ -115,7 +115,7 @@ var exportCmd = &cobra.Command{ if err != nil { util.HandleError(err) } - accessToken = loggedInUserDetails.UserCredentials.JWTToken + accessToken = loggedInUserDetails.UserCredentials.JTWToken } processedTemplate, err := ProcessTemplate(1, templatePath, nil, accessToken, "", &newEtag, dynamicSecretLeases) diff --git a/cli/packages/cmd/init.go b/cli/packages/cmd/init.go index c85cac9f9..2ef555a82 100644 --- a/cli/packages/cmd/init.go +++ b/cli/packages/cmd/init.go @@ -53,7 +53,7 @@ var initCmd = &cobra.Command{ if err != nil { util.HandleError(err, "Unable to get resty client with custom headers") } - httpClient.SetAuthToken(userCreds.UserCredentials.JWTToken) + httpClient.SetAuthToken(userCreds.UserCredentials.JTWToken) organizationResponse, err := api.CallGetAllOrganizations(httpClient) if err != nil { @@ -124,7 +124,7 @@ var initCmd = &cobra.Command{ } // set the config jwt token to the new token - userCreds.UserCredentials.JWTToken = tokenResponse.Token + userCreds.UserCredentials.JTWToken = tokenResponse.Token err = util.StoreUserCredsInKeyRing(&userCreds.UserCredentials) httpClient.SetAuthToken(tokenResponse.Token) diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index c7168066b..fd3ce1569 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -111,7 +111,7 @@ var loginCmd = &cobra.Command{ infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ SiteUrl: config.INFISICAL_URL, UserAgent: api.USER_AGENT, - AutoTokenRefresh: true, + AutoTokenRefresh: false, CustomHeaders: customHeaders, }) @@ -437,8 +437,7 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) { //updating usercredentials userCredentialsToBeStored.Email = email userCredentialsToBeStored.PrivateKey = string(decryptedPrivateKey) - userCredentialsToBeStored.JWTToken = newJwtToken - userCredentialsToBeStored.RefreshToken = loginTwoResponse.RefreshToken + userCredentialsToBeStored.JTWToken = newJwtToken } func init() { @@ -863,7 +862,7 @@ func askToPasteJwtToken(success chan models.UserCredentials, failure chan error) os.Exit(1) } - // verify JWT + // verify JTW httpClient, err := util.GetRestyClientWithCustomHeaders() if err != nil { failure <- err @@ -872,7 +871,7 @@ func askToPasteJwtToken(success chan models.UserCredentials, failure chan error) } httpClient. - SetAuthToken(userCredentials.JWTToken). + SetAuthToken(userCredentials.JTWToken). SetHeader("Accept", "application/json") isAuthenticated := api.CallIsAuthenticated(httpClient) diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 05d1c2d5f..930a27a56 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -245,7 +245,7 @@ var secretsSetCmd = &cobra.Command{ secretOperations, err = util.SetRawSecrets(processedArgs, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{ Type: "", - Token: loggedInUserDetails.UserCredentials.JWTToken, + Token: loggedInUserDetails.UserCredentials.JTWToken, }, file) } @@ -330,7 +330,7 @@ var secretsDeleteCmd = &cobra.Command{ loggedInUserDetails = util.EstablishUserLoginSession() } - httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JWTToken) + httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken) } for _, secretName := range args { diff --git a/cli/packages/cmd/ssh.go b/cli/packages/cmd/ssh.go index e0939995e..4315989bd 100644 --- a/cli/packages/cmd/ssh.go +++ b/cli/packages/cmd/ssh.go @@ -186,7 +186,7 @@ func issueCredentials(cmd *cobra.Command, args []string) { if loggedInUserDetails.LoginExpired { loggedInUserDetails = util.EstablishUserLoginSession() } - infisicalToken = loggedInUserDetails.UserCredentials.JWTToken + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken } certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId") @@ -419,7 +419,7 @@ func signKey(cmd *cobra.Command, args []string) { if loggedInUserDetails.LoginExpired { loggedInUserDetails = util.EstablishUserLoginSession() } - infisicalToken = loggedInUserDetails.UserCredentials.JWTToken + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken } certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId") @@ -628,7 +628,7 @@ func sshConnect(cmd *cobra.Command, args []string) { if loggedInUserDetails.LoginExpired { loggedInUserDetails = util.EstablishUserLoginSession() } - infisicalToken = loggedInUserDetails.UserCredentials.JWTToken + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken } writeHostCaToFile, err := cmd.Flags().GetBool("write-host-ca-to-file") @@ -881,7 +881,7 @@ func sshAddHost(cmd *cobra.Command, args []string) { if loggedInUserDetails.LoginExpired { loggedInUserDetails = util.EstablishUserLoginSession() } - infisicalToken = loggedInUserDetails.UserCredentials.JWTToken + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken } projectId, err := cmd.Flags().GetString("projectId") diff --git a/cli/packages/cmd/tokens.go b/cli/packages/cmd/tokens.go index f867169f8..a2e445239 100644 --- a/cli/packages/cmd/tokens.go +++ b/cli/packages/cmd/tokens.go @@ -115,7 +115,7 @@ var tokensCreateCmd = &cobra.Command{ } } - workspaceKey, err := util.GetPlainTextWorkspaceKey(loggedInUserDetails.UserCredentials.JWTToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceId) + workspaceKey, err := util.GetPlainTextWorkspaceKey(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceId) if err != nil { util.HandleError(err, "Unable to get workspace key needed to create service token") } @@ -140,7 +140,7 @@ var tokensCreateCmd = &cobra.Command{ util.HandleError(err, "Unable to get resty client with custom headers") } - httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JWTToken). + httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). SetHeader("Accept", "application/json") createServiceTokenResponse, err := api.CallCreateServiceToken(httpClient, api.CreateServiceTokenRequest{ diff --git a/cli/packages/cmd/user.go b/cli/packages/cmd/user.go index bde0b3075..6c7d54d46 100644 --- a/cli/packages/cmd/user.go +++ b/cli/packages/cmd/user.go @@ -118,7 +118,7 @@ var userGetTokenCmd = &cobra.Command{ util.HandleError(err, "[infisical user get token]: Unable to get logged in user token") } - tokenParts := strings.Split(loggedInUserDetails.UserCredentials.JWTToken, ".") + tokenParts := strings.Split(loggedInUserDetails.UserCredentials.JTWToken, ".") if len(tokenParts) != 3 { util.HandleError(errors.New("invalid token format"), "[infisical user get token]: Invalid token format") } @@ -136,7 +136,7 @@ var userGetTokenCmd = &cobra.Command{ } fmt.Println("Session ID:", tokenPayload.TokenVersionId) - fmt.Println("Token:", loggedInUserDetails.UserCredentials.JWTToken) + fmt.Println("Token:", loggedInUserDetails.UserCredentials.JTWToken) }, } diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 9a3ff85b2..8b9fef6f6 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -5,8 +5,8 @@ import "time" type UserCredentials struct { Email string `json:"email"` PrivateKey string `json:"privateKey"` - JWTToken string `json:"JWTToken"` - RefreshToken string `json:"refreshToken"` + JTWToken string `json:"JTWToken"` + RefreshToken string `json:"RefreshToken"` } // The file struct for Infisical config file diff --git a/cli/packages/util/constants.go b/cli/packages/util/constants.go index 126e5a5d0..383c7fc4c 100644 --- a/cli/packages/util/constants.go +++ b/cli/packages/util/constants.go @@ -10,6 +10,10 @@ const ( INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN" INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase. + INFISICAL_BOOTSTRAP_EMAIL_NAME = "INFISICAL_ADMIN_EMAIL" + INFISICAL_BOOTSTRAP_PASSWORD_NAME = "INFISICAL_ADMIN_PASSWORD" + INFISICAL_BOOTSTRAP_ORGANIZATION_NAME = "INFISICAL_ADMIN_ORGANIZATION" + VAULT_BACKEND_AUTO_MODE = "auto" VAULT_BACKEND_FILE_MODE = "file" @@ -47,6 +51,11 @@ const ( INFISICAL_BACKUP_SECRET = "infisical-backup-secrets" // akhilmhdh: @depreciated remove in version v0.30 INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY = "infisical-backup-secret-encryption-key" + + KUBERNETES_SERVICE_HOST_ENV_NAME = "KUBERNETES_SERVICE_HOST" + KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME = "KUBERNETES_SERVICE_PORT_HTTPS" + KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token" ) var ( diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index 58de59dee..cd73e47ca 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -9,7 +9,6 @@ import ( "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/models" - "github.com/rs/zerolog/log" "github.com/zalando/go-keyring" ) @@ -91,22 +90,23 @@ func GetCurrentLoggedInUserDetails(setConfigVariables bool) (LoggedInUserDetails } httpClient. - SetAuthToken(userCreds.JWTToken). + SetAuthToken(userCreds.JTWToken). SetHeader("Accept", "application/json") isAuthenticated := api.CallIsAuthenticated(httpClient) - if !isAuthenticated { - accessTokenResponse, refreshErr := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken) - if refreshErr == nil && accessTokenResponse.Token != "" { - isAuthenticated = true - userCreds.JWTToken = accessTokenResponse.Token - } - } + // TODO: add refresh token + // if !isAuthenticated { + // accessTokenResponse, err := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken) + // if err == nil && accessTokenResponse.Token != "" { + // isAuthenticated = true + // userCreds.JTWToken = accessTokenResponse.Token + // } + // } - err = StoreUserCredsInKeyRing(&userCreds) - if err != nil { - log.Debug().Msg("unable to store your user credentials with new access token") - } + // err = StoreUserCredsInKeyRing(&userCreds) + // if err != nil { + // log.Debug().Msg("unable to store your user credentials with new access token") + // } if !isAuthenticated { return LoggedInUserDetails{ diff --git a/cli/packages/util/folders.go b/cli/packages/util/folders.go index 412fadb54..fb4f2a322 100644 --- a/cli/packages/util/folders.go +++ b/cli/packages/util/folders.go @@ -35,7 +35,7 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder params.WorkspaceId = workspaceFile.WorkspaceId } - folders, err := GetFoldersViaJWT(loggedInUserDetails.UserCredentials.JWTToken, params.WorkspaceId, params.Environment, params.FoldersPath) + folders, err := GetFoldersViaJTW(loggedInUserDetails.UserCredentials.JTWToken, params.WorkspaceId, params.Environment, params.FoldersPath) folderErr = err foldersToReturn = folders } else if params.InfisicalToken != "" { @@ -60,14 +60,14 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder return foldersToReturn, folderErr } -func GetFoldersViaJWT(JWTToken string, workspaceId string, environmentName string, foldersPath string) ([]models.SingleFolder, error) { +func GetFoldersViaJTW(JTWToken string, workspaceId string, environmentName string, foldersPath string) ([]models.SingleFolder, error) { // set up resty client httpClient, err := GetRestyClientWithCustomHeaders() if err != nil { return nil, err } - httpClient.SetAuthToken(JWTToken). + httpClient.SetAuthToken(JTWToken). SetHeader("Accept", "application/json") getFoldersRequest := api.GetFoldersV1Request{ @@ -194,7 +194,7 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er loggedInUserDetails = EstablishUserLoginSession() } - params.InfisicalToken = loggedInUserDetails.UserCredentials.JWTToken + params.InfisicalToken = loggedInUserDetails.UserCredentials.JTWToken } // set up resty client @@ -243,7 +243,7 @@ func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder, loggedInUserDetails = EstablishUserLoginSession() } - params.InfisicalToken = loggedInUserDetails.UserCredentials.JWTToken + params.InfisicalToken = loggedInUserDetails.UserCredentials.JTWToken } // set up resty client diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 4666291e7..814e7da23 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -302,9 +302,9 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo params.WorkspaceId = infisicalDotJson.WorkspaceId } - res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JWTToken, params.WorkspaceId, + res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JTWToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs, true) - log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JWT token [err=%s]", err) + log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", err) if err == nil { backupEncryptionKey, err := GetBackupEncryptionKey() diff --git a/docs/api-reference/endpoints/app-connections/cloudflare/available.mdx b/docs/api-reference/endpoints/app-connections/cloudflare/available.mdx new file mode 100644 index 000000000..bc61552bf --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/cloudflare/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/cloudflare/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/cloudflare/create.mdx b/docs/api-reference/endpoints/app-connections/cloudflare/create.mdx new file mode 100644 index 000000000..a364b2268 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/cloudflare/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/cloudflare" +--- + + + Check out the configuration docs for [Cloudflare + Connections](/integrations/app-connections/cloudflare) to learn how to obtain + the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/cloudflare/delete.mdx b/docs/api-reference/endpoints/app-connections/cloudflare/delete.mdx new file mode 100644 index 000000000..d0d766717 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/cloudflare/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/cloudflare/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/cloudflare/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/cloudflare/get-by-id.mdx new file mode 100644 index 000000000..b1590da75 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/cloudflare/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/cloudflare/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/cloudflare/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/cloudflare/get-by-name.mdx new file mode 100644 index 000000000..b3613d079 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/cloudflare/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/cloudflare/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/cloudflare/list.mdx b/docs/api-reference/endpoints/app-connections/cloudflare/list.mdx new file mode 100644 index 000000000..df0189c45 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/cloudflare/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/cloudflare" +--- diff --git a/docs/api-reference/endpoints/app-connections/cloudflare/update.mdx b/docs/api-reference/endpoints/app-connections/cloudflare/update.mdx new file mode 100644 index 000000000..dbd70acde --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/cloudflare/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/cloudflare/{connectionId}" +--- + + + Check out the configuration docs for [Cloudflare + Connections](/integrations/app-connections/cloudflare) to learn how to obtain + the required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/create.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/create.mdx new file mode 100644 index 000000000..c30499bfc --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/cloudflare-pages" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/delete.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/delete.mdx new file mode 100644 index 000000000..2ff903032 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/cloudflare-pages/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/get-by-id.mdx new file mode 100644 index 000000000..b6c81cb5e --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/cloudflare-pages/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/get-by-name.mdx new file mode 100644 index 000000000..56d69b5b4 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/cloudflare-pages/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/list.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/list.mdx new file mode 100644 index 000000000..631f06ec6 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/cloudflare-pages" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/remove-secrets.mdx new file mode 100644 index 000000000..b113a86dc --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/cloudflare-pages/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/sync-secrets.mdx new file mode 100644 index 000000000..53d5e4b85 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/cloudflare-pages/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/update.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/update.mdx new file mode 100644 index 000000000..4f11328d0 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-pages/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/cloudflare-pages/{syncId}" +--- diff --git a/docs/cli/commands/bootstrap.mdx b/docs/cli/commands/bootstrap.mdx index 77f8b38f1..ae31e470f 100644 --- a/docs/cli/commands/bootstrap.mdx +++ b/docs/cli/commands/bootstrap.mdx @@ -75,8 +75,90 @@ This flag is required. + + Whether to continue without error if the instance has already been bootstrapped. Useful for idempotent automation scripts. + +```bash +# Example +infisical bootstrap --ignore-if-bootstrapped +``` + +This flag is optional and defaults to `false`. + + + + + The type of output format for the bootstrap command. Supports `k8-secret` for Kubernetes secret integration. This flag is optional and defaults to "". + +```bash +# Kubernetes secret output +infisical bootstrap --output=k8-secret --k8-secret-template='{"data":{"token":"{{.Identity.Credentials.Token}}"}}' --k8-secret-name=infisical-bootstrap --k8-secret-namespace=default +``` + +When using `k8-secret`, the command will create or update a Kubernetes secret directly in your cluster. Note that this option requires the command to be executed from within a Kubernetes pod with appropriate service account permissions. + + + + + The template to use for rendering the Kubernetes secret data/stringData section. Required when using `--output=k8-secret`. The template uses Go template syntax and has access to the bootstrap response data. + +```bash +# Example template that stores the token +infisical bootstrap --k8-secret-template='{"data":{"token":"{{.Identity.Credentials.Token}}"}}' + +# Example template with multiple fields +infisical bootstrap --k8-secret-template='{"stringData":{"token":"{{.Identity.Credentials.Token}}","org-id":"{{.Organization.ID}}","user-email":"{{.User.Email}}"}}' +``` + +Available template functions: + +- `encodeBase64`: Base64 encode a string + +Available data fields: + +- `.Identity.Credentials.Token`: The machine identity token +- `.Identity.ID`: The identity ID +- `.Identity.Name`: The identity name +- `.Organization.ID`: The organization ID +- `.Organization.Name`: The organization name +- `.Organization.Slug`: The organization slug +- `.User.Email`: The admin user email +- `.User.ID`: The admin user ID +- `.User.FirstName`: The admin user first name +- `.User.LastName`: The admin user last name + +This flag is required when using `k8-secret` output. + + + + + The name of the Kubernetes secret to create or update. Required when using `--output=k8-secret`. + +```bash +# Example +infisical bootstrap --k8-secret-name=infisical-bootstrap-credentials +``` + +This flag is required when using `k8-secret` output. + + + + + The namespace where the Kubernetes secret should be created or updated. Required when using `--output=k8-secret`. + +```bash +# Example +infisical bootstrap --k8-secret-namespace=infisical-system +``` + +This flag is required when using `k8-secret` output. + + + ## Response +### JSON Output (Default) + The command returns a JSON response with details about the created user, organization, and machine identity: ```json @@ -105,6 +187,47 @@ The command returns a JSON response with details about the created user, organiz } ``` +### Kubernetes Secret Output + +When using `--output=k8-secret`, the command creates or updates a Kubernetes secret in your cluster and logs the operation result. This is particularly useful for automated bootstrapping scenarios such as Kubernetes Jobs, GitOps workflows, or when you need to immediately store the admin credentials for use by other applications in your cluster. + +## Kubernetes Integration + +### Prerequisites for k8-secret Output + +When running with `--output=k8-secret`, the command must be executed from within a Kubernetes pod with proper service account permissions. The command automatically: + +1. Reads the service account token from `/var/run/secrets/kubernetes.io/serviceaccount/token` +2. Reads the CA certificate from `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` +3. Gets the Kubernetes API server URL from environment variables (`KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT_HTTPS`) + +### Required RBAC Permissions + +Your service account needs the following permissions: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: infisical-bootstrap +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: infisical-bootstrap +subjects: + - kind: ServiceAccount + name: your-service-account +roleRef: + kind: Role + name: infisical-bootstrap + apiGroup: rbac.authorization.k8s.io +``` + ## Usage with Automation For automation purposes, you can extract just the machine identity token from the response: @@ -127,6 +250,8 @@ echo "Token has been captured and can be used for authentication" ## Notes - The bootstrap process can only be performed once on a fresh Infisical instance -- All flags are required for the bootstrap process to complete successfully +- All core flags (domain, email, password, organization) are required for the bootstrap process to complete successfully - Security controls prevent privilege escalation: instance admin identities cannot be managed by non-instance admin users and identities - The generated admin user account can be used to log in via the UI if needed +- When using `k8-secret` output, the command must run within a Kubernetes pod with proper service account permissions +- The `--ignore-if-bootstrapped` flag is useful for making bootstrap scripts idempotent diff --git a/docs/docs.json b/docs/docs.json new file mode 100644 index 000000000..dcde2c085 --- /dev/null +++ b/docs/docs.json @@ -0,0 +1,2284 @@ +{ + "$schema": "https://mintlify.com/docs.json", + "theme": "mint", + "name": "Infisical", + "colors": { + "primary": "#26272b", + "light": "#97b31d", + "dark": "#A1B659" + }, + "styling": { + "codeblocks": "dark" + }, + "favicon": "/favicon.png", + "navigation": { + "tabs": [ + { + "tab": "Documentation", + "groups": [ + { + "group": "Getting Started", + "pages": [ + "documentation/getting-started/introduction", + { + "group": "Quickstart", + "pages": ["documentation/guides/local-development"] + }, + { + "group": "Guides", + "pages": [ + "documentation/guides/introduction", + "documentation/guides/node", + "documentation/guides/python", + "documentation/guides/nextjs-vercel", + "documentation/guides/microsoft-power-apps", + "documentation/guides/organization-structure" + ] + }, + { + "group": "Setup", + "pages": ["documentation/setup/networking"] + } + ] + }, + { + "group": "Platform", + "pages": [ + "documentation/platform/organization", + "documentation/platform/project", + "documentation/platform/folder", + { + "group": "Secrets", + "pages": [ + "documentation/platform/secret-versioning", + "documentation/platform/pit-recovery", + "documentation/platform/secret-reference", + "documentation/platform/webhooks" + ] + }, + { + "group": "Internal PKI", + "pages": [ + "documentation/platform/pki/overview", + "documentation/platform/pki/private-ca", + "documentation/platform/pki/external-ca", + "documentation/platform/pki/subscribers", + "documentation/platform/pki/certificates", + "documentation/platform/pki/acme-ca", + "documentation/platform/pki/est", + "documentation/platform/pki/alerting", + { + "group": "Integrations", + "pages": [ + "documentation/platform/pki/pki-issuer", + "documentation/platform/pki/integration-guides/gloo-mesh" + ] + } + ] + }, + { + "group": "Infisical SSH", + "pages": [ + "documentation/platform/ssh/overview", + "documentation/platform/ssh/host-groups" + ] + }, + { + "group": "Key Management (KMS)", + "pages": [ + "documentation/platform/kms/overview", + "documentation/platform/kms/hsm-integration", + "documentation/platform/kms/kubernetes-encryption", + "documentation/platform/kms/kmip" + ] + }, + { + "group": "KMS Configuration", + "pages": [ + "documentation/platform/kms-configuration/overview", + "documentation/platform/kms-configuration/aws-kms", + "documentation/platform/kms-configuration/aws-hsm", + "documentation/platform/kms-configuration/gcp-kms" + ] + }, + { + "group": "Identities", + "pages": [ + "documentation/platform/identities/overview", + "documentation/platform/identities/user-identities", + "documentation/platform/identities/machine-identities" + ] + }, + { + "group": "Access Control", + "pages": [ + "documentation/platform/access-controls/overview", + "documentation/platform/access-controls/role-based-access-controls", + { + "group": "Attribute based access controls", + "pages": [ + "documentation/platform/access-controls/abac/overview", + "documentation/platform/access-controls/abac/managing-user-metadata", + "documentation/platform/access-controls/abac/managing-machine-identity-attributes" + ] + }, + "documentation/platform/access-controls/additional-privileges", + "documentation/platform/access-controls/temporary-access", + "documentation/platform/access-controls/assume-privilege", + "documentation/platform/access-controls/access-requests", + "documentation/platform/access-controls/project-access-requests", + "documentation/platform/pr-workflows", + "documentation/platform/groups" + ] + }, + { + "group": "Audit Logs", + "pages": [ + "documentation/platform/audit-logs", + "documentation/platform/audit-log-streams/audit-log-streams", + "documentation/platform/audit-log-streams/audit-log-streams-with-fluentbit" + ] + }, + { + "group": "Secret Rotation", + "pages": [ + "documentation/platform/secret-rotation/overview", + "documentation/platform/secret-rotation/auth0-client-secret", + "documentation/platform/secret-rotation/aws-iam-user-secret", + "documentation/platform/secret-rotation/azure-client-secret", + "documentation/platform/secret-rotation/ldap-password", + "documentation/platform/secret-rotation/mssql-credentials", + "documentation/platform/secret-rotation/mysql-credentials", + "documentation/platform/secret-rotation/oracledb-credentials", + "documentation/platform/secret-rotation/postgres-credentials" + ] + }, + { + "group": "Dynamic Secrets", + "pages": [ + "documentation/platform/dynamic-secrets/overview", + "documentation/platform/dynamic-secrets/aws-elasticache", + "documentation/platform/dynamic-secrets/aws-iam", + "documentation/platform/dynamic-secrets/azure-entra-id", + "documentation/platform/dynamic-secrets/cassandra", + "documentation/platform/dynamic-secrets/elastic-search", + "documentation/platform/dynamic-secrets/gcp-iam", + "documentation/platform/dynamic-secrets/github", + "documentation/platform/dynamic-secrets/ldap", + "documentation/platform/dynamic-secrets/mongo-atlas", + "documentation/platform/dynamic-secrets/mongo-db", + "documentation/platform/dynamic-secrets/mssql", + "documentation/platform/dynamic-secrets/mysql", + "documentation/platform/dynamic-secrets/oracle", + "documentation/platform/dynamic-secrets/postgresql", + "documentation/platform/dynamic-secrets/rabbit-mq", + "documentation/platform/dynamic-secrets/redis", + "documentation/platform/dynamic-secrets/sap-ase", + "documentation/platform/dynamic-secrets/sap-hana", + "documentation/platform/dynamic-secrets/snowflake", + "documentation/platform/dynamic-secrets/totp", + "documentation/platform/dynamic-secrets/kubernetes", + "documentation/platform/dynamic-secrets/vertica" + ] + }, + { + "group": "Gateway", + "pages": [ + "documentation/platform/gateways/overview", + "documentation/platform/gateways/gateway-security", + "documentation/platform/gateways/networking" + ] + }, + "documentation/platform/project-templates", + { + "group": "Workflow Integrations", + "pages": [ + "documentation/platform/workflow-integrations/slack-integration", + "documentation/platform/workflow-integrations/microsoft-teams-integration" + ] + }, + { + "group": "Admin Consoles", + "pages": [ + "documentation/platform/admin-panel/overview", + "documentation/platform/admin-panel/server-admin", + "documentation/platform/admin-panel/org-admin-console" + ] + }, + "documentation/platform/secret-sharing", + { + "group": "Secret Scanning", + "pages": [ + "documentation/platform/secret-scanning/overview", + "documentation/platform/secret-scanning/github" + ] + } + ] + }, + { + "group": "Authentication Methods", + "pages": [ + { + "group": "User Authentication", + "pages": [ + "documentation/platform/auth-methods/email-password", + { + "group": "SSO", + "pages": [ + "documentation/platform/sso/overview", + "documentation/platform/sso/google", + "documentation/platform/sso/github", + "documentation/platform/sso/gitlab", + "documentation/platform/sso/okta", + "documentation/platform/sso/azure", + "documentation/platform/sso/jumpcloud", + "documentation/platform/sso/keycloak-saml", + "documentation/platform/sso/google-saml", + "documentation/platform/sso/auth0-saml", + { + "group": "OIDC", + "pages": [ + { + "group": "Keycloak OIDC", + "pages": [ + "documentation/platform/sso/keycloak-oidc/overview", + "documentation/platform/sso/keycloak-oidc/group-membership-mapping" + ] + }, + "documentation/platform/sso/auth0-oidc", + { + "group": "General OIDC", + "pages": [ + "documentation/platform/sso/general-oidc/overview", + "documentation/platform/sso/general-oidc/group-membership-mapping" + ] + } + ] + } + ] + }, + { + "group": "LDAP", + "pages": [ + "documentation/platform/ldap/overview", + "documentation/platform/ldap/jumpcloud", + "documentation/platform/ldap/general" + ] + }, + { + "group": "SCIM", + "pages": [ + "documentation/platform/scim/overview", + "documentation/platform/scim/okta", + "documentation/platform/scim/azure", + "documentation/platform/scim/jumpcloud", + "documentation/platform/scim/group-mappings" + ] + } + ] + }, + { + "group": "Machine Identities", + "pages": [ + "documentation/platform/identities/alicloud-auth", + "documentation/platform/identities/aws-auth", + "documentation/platform/identities/azure-auth", + "documentation/platform/identities/gcp-auth", + "documentation/platform/identities/jwt-auth", + "documentation/platform/identities/kubernetes-auth", + "documentation/platform/identities/oci-auth", + "documentation/platform/identities/token-auth", + "documentation/platform/identities/universal-auth", + { + "group": "OIDC Auth", + "pages": [ + "documentation/platform/identities/oidc-auth/general", + "documentation/platform/identities/oidc-auth/azure", + "documentation/platform/identities/oidc-auth/github", + "documentation/platform/identities/oidc-auth/circleci", + "documentation/platform/identities/oidc-auth/gitlab", + "documentation/platform/identities/oidc-auth/terraform-cloud", + "documentation/platform/identities/oidc-auth/spire" + ] + }, + { + "group": "LDAP Auth", + "pages": [ + "documentation/platform/identities/ldap-auth/general", + "documentation/platform/identities/ldap-auth/jumpcloud" + ] + } + ] + }, + "documentation/platform/token", + "documentation/platform/mfa", + "documentation/platform/github-org-sync" + ] + }, + { + "group": "Self-host Infisical", + "pages": [ + "self-hosting/overview", + { + "group": "Installation methods", + "pages": [ + "self-hosting/deployment-options/standalone-infisical", + "self-hosting/deployment-options/docker-swarm", + "self-hosting/deployment-options/docker-compose", + "self-hosting/deployment-options/kubernetes-helm" + ] + }, + { + "group": "Linux Package", + "pages": [ + "self-hosting/deployment-options/native/linux-package/installation", + "self-hosting/deployment-options/native/linux-package/commands-configuration", + "self-hosting/deployment-options/linux-upgrade" + ] + }, + "self-hosting/guides/upgrading-infisical", + "self-hosting/configuration/envars", + "self-hosting/configuration/requirements", + { + "group": "Guides", + "pages": [ + "self-hosting/guides/mongo-to-postgres", + "self-hosting/guides/custom-certificates", + "self-hosting/guides/automated-bootstrapping", + "self-hosting/guides/production-hardening" + ] + }, + { + "group": "Reference architectures", + "pages": [ + "self-hosting/reference-architectures/aws-ecs", + "self-hosting/reference-architectures/linux-deployment-ha", + "self-hosting/reference-architectures/on-prem-k8s-ha", + "self-hosting/reference-architectures/google-cloud-run" + ] + }, + "self-hosting/ee", + "self-hosting/faq" + ] + }, + { + "group": "Internals", + "pages": [ + "internals/overview", + { + "group": "Permissions", + "pages": [ + "internals/permissions/overview", + "internals/permissions/project-permissions", + "internals/permissions/organization-permissions", + "internals/permissions/migration" + ] + }, + { + "group": "Architecture", + "pages": [ + "internals/architecture/components", + "internals/architecture/cloud" + ] + }, + "internals/security", + "internals/service-tokens" + ] + }, + { + "group": "Contributing", + "pages": [ + { + "group": "Getting Started", + "pages": [ + "contributing/getting-started/overview", + "contributing/getting-started/code-of-conduct", + "contributing/getting-started/pull-requests", + "contributing/getting-started/faq" + ] + }, + { + "group": "Contributing to platform", + "pages": [ + "contributing/platform/developing", + "contributing/platform/backend/how-to-create-a-feature", + "contributing/platform/backend/folder-structure" + ] + }, + { + "group": "Contributing to SDK", + "pages": ["contributing/sdk/developing"] + } + ] + } + ] + }, + { + "tab": "Integrations", + "groups": [ + { + "group": "Infrastructure Integrations", + "pages": [ + "integrations/platforms/ansible", + "integrations/platforms/apache-airflow", + { + "group": "Container orchestrators", + "pages": [ + { + "group": "Kubernetes", + "pages": [ + "integrations/platforms/kubernetes/overview", + "integrations/platforms/kubernetes/infisical-secret-crd", + "integrations/platforms/kubernetes/infisical-push-secret-crd", + "integrations/platforms/kubernetes/infisical-dynamic-secret-crd" + ] + }, + "integrations/platforms/kubernetes-injector", + "integrations/platforms/kubernetes-csi", + "integrations/platforms/docker-swarm-with-agent", + "integrations/platforms/ecs-with-agent" + ] + }, + { + "group": "Docker", + "pages": [ + "integrations/platforms/docker-intro", + "integrations/platforms/docker", + "integrations/platforms/docker-pass-envs", + "integrations/platforms/docker-compose" + ] + }, + "integrations/platforms/infisical-agent", + "integrations/frameworks/packer", + "integrations/frameworks/pulumi", + "integrations/frameworks/terraform" + ] + }, + { + "group": "App Connections", + "pages": [ + "integrations/app-connections/overview", + { + "group": "Connections", + "pages": [ + "integrations/app-connections/1password", + "integrations/app-connections/auth0", + "integrations/app-connections/aws", + "integrations/app-connections/azure-app-configuration", + "integrations/app-connections/azure-client-secrets", + "integrations/app-connections/azure-devops", + "integrations/app-connections/azure-key-vault", + "integrations/app-connections/camunda", + "integrations/app-connections/cloudflare", + "integrations/app-connections/databricks", + "integrations/app-connections/flyio", + "integrations/app-connections/gcp", + "integrations/app-connections/github", + "integrations/app-connections/github-radar", + "integrations/app-connections/gitlab", + "integrations/app-connections/hashicorp-vault", + "integrations/app-connections/heroku", + "integrations/app-connections/humanitec", + "integrations/app-connections/ldap", + "integrations/app-connections/mssql", + "integrations/app-connections/mysql", + "integrations/app-connections/oci", + "integrations/app-connections/oracledb", + "integrations/app-connections/postgres", + "integrations/app-connections/render", + "integrations/app-connections/teamcity", + "integrations/app-connections/terraform-cloud", + "integrations/app-connections/vercel", + "integrations/app-connections/windmill" + ] + } + ] + }, + { + "group": "Secret Syncs", + "pages": [ + "integrations/secret-syncs/overview", + { + "group": "Syncs", + "pages": [ + "integrations/secret-syncs/1password", + "integrations/secret-syncs/aws-parameter-store", + "integrations/secret-syncs/aws-secrets-manager", + "integrations/secret-syncs/azure-app-configuration", + "integrations/secret-syncs/azure-devops", + "integrations/secret-syncs/azure-key-vault", + "integrations/secret-syncs/camunda", + "integrations/secret-syncs/cloudflare-pages", + "integrations/secret-syncs/databricks", + "integrations/secret-syncs/flyio", + "integrations/secret-syncs/gcp-secret-manager", + "integrations/secret-syncs/github", + "integrations/secret-syncs/gitlab", + "integrations/secret-syncs/hashicorp-vault", + "integrations/secret-syncs/heroku", + "integrations/secret-syncs/humanitec", + "integrations/secret-syncs/oci-vault", + "integrations/secret-syncs/render", + "integrations/secret-syncs/teamcity", + "integrations/secret-syncs/terraform-cloud", + "integrations/secret-syncs/vercel", + "integrations/secret-syncs/windmill" + ] + } + ] + }, + { + "group": "Native Integrations", + "pages": [ + { + "group": "AWS", + "pages": [ + "integrations/cloud/aws-parameter-store", + "integrations/cloud/aws-secret-manager", + "integrations/cloud/aws-amplify" + ] + }, + "integrations/cloud/vercel", + "integrations/cloud/azure-key-vault", + "integrations/cloud/azure-app-configuration", + "integrations/cloud/azure-devops", + "integrations/cloud/gcp-secret-manager", + { + "group": "Cloudflare", + "pages": [ + "integrations/cloud/cloudflare-pages", + "integrations/cloud/cloudflare-workers" + ] + }, + "integrations/cloud/terraform-cloud", + "integrations/cloud/databricks", + { + "group": "View more", + "pages": [ + "integrations/cloud/digital-ocean-app-platform", + "integrations/cloud/heroku", + "integrations/cloud/netlify", + "integrations/cloud/railway", + "integrations/cloud/flyio", + "integrations/cloud/render", + "integrations/cloud/laravel-forge", + "integrations/cloud/supabase", + "integrations/cloud/northflank", + "integrations/cloud/hasura-cloud", + "integrations/cloud/qovery", + "integrations/cloud/hashicorp-vault", + "integrations/cloud/cloud-66", + "integrations/cloud/windmill" + ] + } + ] + }, + { + "group": "CI/CD Integrations", + "pages": [ + "integrations/cicd/jenkins", + "integrations/cicd/githubactions", + "integrations/cicd/gitlab", + "integrations/cicd/bitbucket", + "integrations/cloud/teamcity", + { + "group": "View more", + "pages": [ + "integrations/cicd/circleci", + "integrations/cicd/travisci", + "integrations/cicd/rundeck", + "integrations/cicd/codefresh", + "integrations/cloud/checkly", + "integrations/cicd/octopus-deploy" + ] + } + ] + }, + { + "group": "Framework Integrations", + "pages": [ + "integrations/frameworks/spring-boot-maven", + "integrations/frameworks/react", + "integrations/frameworks/vue", + "integrations/frameworks/express", + { + "group": "View more", + "pages": [ + "integrations/frameworks/nextjs", + "integrations/frameworks/nestjs", + "integrations/frameworks/sveltekit", + "integrations/frameworks/nuxt", + "integrations/frameworks/gatsby", + "integrations/frameworks/remix", + "integrations/frameworks/vite", + "integrations/frameworks/fiber", + "integrations/frameworks/django", + "integrations/frameworks/flask", + "integrations/frameworks/laravel", + "integrations/frameworks/rails", + "integrations/frameworks/dotnet", + "integrations/platforms/pm2", + "integrations/frameworks/ab-initio" + ] + } + ] + }, + { + "group": "Build Tool Integrations", + "pages": ["integrations/build-tools/gradle"] + }, + { + "group": "Others", + "pages": ["integrations/external/backstage"] + } + ] + }, + { + "tab": "CLI", + "groups": [ + { + "group": "Command line", + "pages": [ + "cli/overview", + "cli/usage", + { + "group": "Core commands", + "pages": [ + "cli/commands/login", + "cli/commands/init", + "cli/commands/run", + "cli/commands/secrets", + "cli/commands/dynamic-secrets", + "cli/commands/ssh", + "cli/commands/gateway", + "cli/commands/bootstrap", + "cli/commands/export", + "cli/commands/token", + "cli/commands/service-token", + "cli/commands/vault", + "cli/commands/user", + "cli/commands/reset", + { + "group": "infisical scan", + "pages": [ + "cli/commands/scan", + "cli/commands/scan-git-changes", + "cli/commands/scan-install" + ] + } + ] + }, + "cli/scanning-overview", + "cli/project-config", + "cli/faq" + ] + } + ] + }, + { + "tab": "API Reference", + "groups": [ + { + "group": "Overview", + "pages": [ + "api-reference/overview/introduction", + "api-reference/overview/authentication", + { + "group": "Examples", + "pages": ["api-reference/overview/examples/integration"] + } + ] + }, + { + "group": "Endpoints", + "pages": [ + { + "group": "Identities", + "pages": [ + "api-reference/endpoints/identities/create", + "api-reference/endpoints/identities/update", + "api-reference/endpoints/identities/delete", + "api-reference/endpoints/identities/get-by-id", + "api-reference/endpoints/identities/list", + "api-reference/endpoints/identities/search" + ] + }, + { + "group": "Token Auth", + "pages": [ + "api-reference/endpoints/token-auth/attach", + "api-reference/endpoints/token-auth/retrieve", + "api-reference/endpoints/token-auth/update", + "api-reference/endpoints/token-auth/revoke", + "api-reference/endpoints/token-auth/get-tokens", + "api-reference/endpoints/token-auth/create-token", + "api-reference/endpoints/token-auth/update-token", + "api-reference/endpoints/token-auth/revoke-token" + ] + }, + { + "group": "Universal Auth", + "pages": [ + "api-reference/endpoints/universal-auth/login", + "api-reference/endpoints/universal-auth/attach", + "api-reference/endpoints/universal-auth/retrieve", + "api-reference/endpoints/universal-auth/update", + "api-reference/endpoints/universal-auth/revoke", + "api-reference/endpoints/universal-auth/create-client-secret", + "api-reference/endpoints/universal-auth/list-client-secrets", + "api-reference/endpoints/universal-auth/revoke-client-secret", + "api-reference/endpoints/universal-auth/get-client-secret-by-id", + "api-reference/endpoints/universal-auth/renew-access-token", + "api-reference/endpoints/universal-auth/revoke-access-token" + ] + }, + { + "group": "GCP Auth", + "pages": [ + "api-reference/endpoints/gcp-auth/login", + "api-reference/endpoints/gcp-auth/attach", + "api-reference/endpoints/gcp-auth/retrieve", + "api-reference/endpoints/gcp-auth/update", + "api-reference/endpoints/gcp-auth/revoke" + ] + }, + { + "group": "Alibaba Cloud Auth", + "pages": [ + "api-reference/endpoints/alicloud-auth/login", + "api-reference/endpoints/alicloud-auth/attach", + "api-reference/endpoints/alicloud-auth/retrieve", + "api-reference/endpoints/alicloud-auth/update", + "api-reference/endpoints/alicloud-auth/revoke" + ] + }, + { + "group": "AWS Auth", + "pages": [ + "api-reference/endpoints/aws-auth/login", + "api-reference/endpoints/aws-auth/attach", + "api-reference/endpoints/aws-auth/retrieve", + "api-reference/endpoints/aws-auth/update", + "api-reference/endpoints/aws-auth/revoke" + ] + }, + { + "group": "OCI Auth", + "pages": [ + "api-reference/endpoints/oci-auth/login", + "api-reference/endpoints/oci-auth/attach", + "api-reference/endpoints/oci-auth/retrieve", + "api-reference/endpoints/oci-auth/update", + "api-reference/endpoints/oci-auth/revoke" + ] + }, + { + "group": "Azure Auth", + "pages": [ + "api-reference/endpoints/azure-auth/login", + "api-reference/endpoints/azure-auth/attach", + "api-reference/endpoints/azure-auth/retrieve", + "api-reference/endpoints/azure-auth/update", + "api-reference/endpoints/azure-auth/revoke" + ] + }, + { + "group": "Kubernetes Auth", + "pages": [ + "api-reference/endpoints/kubernetes-auth/login", + "api-reference/endpoints/kubernetes-auth/attach", + "api-reference/endpoints/kubernetes-auth/retrieve", + "api-reference/endpoints/kubernetes-auth/update", + "api-reference/endpoints/kubernetes-auth/revoke" + ] + }, + { + "group": "OIDC Auth", + "pages": [ + "api-reference/endpoints/oidc-auth/login", + "api-reference/endpoints/oidc-auth/attach", + "api-reference/endpoints/oidc-auth/retrieve", + "api-reference/endpoints/oidc-auth/update", + "api-reference/endpoints/oidc-auth/revoke" + ] + }, + { + "group": "JWT Auth", + "pages": [ + "api-reference/endpoints/jwt-auth/login", + "api-reference/endpoints/jwt-auth/attach", + "api-reference/endpoints/jwt-auth/retrieve", + "api-reference/endpoints/jwt-auth/update", + "api-reference/endpoints/jwt-auth/revoke" + ] + }, + { + "group": "LDAP Auth", + "pages": [ + "api-reference/endpoints/ldap-auth/login", + "api-reference/endpoints/ldap-auth/attach", + "api-reference/endpoints/ldap-auth/retrieve", + "api-reference/endpoints/ldap-auth/update", + "api-reference/endpoints/ldap-auth/revoke" + ] + }, + { + "group": "Groups", + "pages": [ + "api-reference/endpoints/groups/create", + "api-reference/endpoints/groups/update", + "api-reference/endpoints/groups/delete", + "api-reference/endpoints/groups/get", + "api-reference/endpoints/groups/get-by-id", + "api-reference/endpoints/groups/add-group-user", + "api-reference/endpoints/groups/remove-group-user", + "api-reference/endpoints/groups/list-group-users" + ] + }, + { + "group": "Organizations", + "pages": [ + "api-reference/endpoints/organizations/memberships", + "api-reference/endpoints/organizations/update-membership", + "api-reference/endpoints/organizations/delete-membership", + "api-reference/endpoints/organizations/list-identity-memberships", + "api-reference/endpoints/organizations/workspaces" + ] + }, + { + "group": "Projects", + "pages": [ + "api-reference/endpoints/workspaces/create-workspace", + "api-reference/endpoints/workspaces/delete-workspace", + "api-reference/endpoints/workspaces/get-workspace", + "api-reference/endpoints/workspaces/update-workspace", + "api-reference/endpoints/workspaces/secret-snapshots" + ] + }, + { + "group": "Project Users", + "pages": [ + "api-reference/endpoints/project-users/invite-member-to-workspace", + "api-reference/endpoints/project-users/remove-member-from-workspace", + "api-reference/endpoints/project-users/memberships", + "api-reference/endpoints/project-users/get-by-username", + "api-reference/endpoints/project-users/update-membership" + ] + }, + { + "group": "Project Groups", + "pages": [ + "api-reference/endpoints/project-groups/create", + "api-reference/endpoints/project-groups/delete", + "api-reference/endpoints/project-groups/get-by-id", + "api-reference/endpoints/project-groups/list", + "api-reference/endpoints/project-groups/update" + ] + }, + { + "group": "Project Identities", + "pages": [ + "api-reference/endpoints/project-identities/add-identity-membership", + "api-reference/endpoints/project-identities/list-identity-memberships", + "api-reference/endpoints/project-identities/get-by-id", + "api-reference/endpoints/project-identities/update-identity-membership", + "api-reference/endpoints/project-identities/delete-identity-membership" + ] + }, + { + "group": "Project Roles", + "pages": [ + "api-reference/endpoints/project-roles/create", + "api-reference/endpoints/project-roles/update", + "api-reference/endpoints/project-roles/delete", + "api-reference/endpoints/project-roles/get-by-slug", + "api-reference/endpoints/project-roles/list" + ] + }, + { + "group": "Project Templates", + "pages": [ + "api-reference/endpoints/project-templates/create", + "api-reference/endpoints/project-templates/update", + "api-reference/endpoints/project-templates/delete", + "api-reference/endpoints/project-templates/get-by-id", + "api-reference/endpoints/project-templates/list" + ] + }, + { + "group": "Environments", + "pages": [ + "api-reference/endpoints/environments/create", + "api-reference/endpoints/environments/update", + "api-reference/endpoints/environments/delete" + ] + }, + { + "group": "Folders", + "pages": [ + "api-reference/endpoints/folders/list", + "api-reference/endpoints/folders/get-by-id", + "api-reference/endpoints/folders/create", + "api-reference/endpoints/folders/update", + "api-reference/endpoints/folders/delete" + ] + }, + { + "group": "Secret Tags", + "pages": [ + "api-reference/endpoints/secret-tags/list", + "api-reference/endpoints/secret-tags/get-by-id", + "api-reference/endpoints/secret-tags/get-by-slug", + "api-reference/endpoints/secret-tags/create", + "api-reference/endpoints/secret-tags/update", + "api-reference/endpoints/secret-tags/delete" + ] + }, + { + "group": "Secrets", + "pages": [ + "api-reference/endpoints/secrets/list", + "api-reference/endpoints/secrets/create", + "api-reference/endpoints/secrets/read", + "api-reference/endpoints/secrets/update", + "api-reference/endpoints/secrets/delete", + "api-reference/endpoints/secrets/create-many", + "api-reference/endpoints/secrets/update-many", + "api-reference/endpoints/secrets/delete-many", + "api-reference/endpoints/secrets/attach-tags", + "api-reference/endpoints/secrets/detach-tags" + ] + }, + { + "group": "Dynamic Secrets", + "pages": [ + { + "group": "Kubernetes", + "pages": [ + "api-reference/endpoints/dynamic-secrets/kubernetes/create-lease" + ] + }, + "api-reference/endpoints/dynamic-secrets/create", + "api-reference/endpoints/dynamic-secrets/update", + "api-reference/endpoints/dynamic-secrets/delete", + "api-reference/endpoints/dynamic-secrets/get", + "api-reference/endpoints/dynamic-secrets/list", + "api-reference/endpoints/dynamic-secrets/list-leases", + "api-reference/endpoints/dynamic-secrets/create-lease", + "api-reference/endpoints/dynamic-secrets/delete-lease", + "api-reference/endpoints/dynamic-secrets/renew-lease", + "api-reference/endpoints/dynamic-secrets/get-lease" + ] + }, + { + "group": "Secret Imports", + "pages": [ + "api-reference/endpoints/secret-imports/list", + "api-reference/endpoints/secret-imports/create", + "api-reference/endpoints/secret-imports/update", + "api-reference/endpoints/secret-imports/delete" + ] + }, + { + "group": "Secret Rotations", + "pages": [ + "api-reference/endpoints/secret-rotations/list", + "api-reference/endpoints/secret-rotations/options", + { + "group": "Auth0 Client Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/auth0-client-secret/create", + "api-reference/endpoints/secret-rotations/auth0-client-secret/delete", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-id", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-by-name", + "api-reference/endpoints/secret-rotations/auth0-client-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/auth0-client-secret/list", + "api-reference/endpoints/secret-rotations/auth0-client-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/auth0-client-secret/update" + ] + }, + { + "group": "AWS IAM User Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/create", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/delete", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-id", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-by-name", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/list", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/aws-iam-user-secret/update" + ] + }, + { + "group": "Azure Client Secret", + "pages": [ + "api-reference/endpoints/secret-rotations/azure-client-secret/create", + "api-reference/endpoints/secret-rotations/azure-client-secret/delete", + "api-reference/endpoints/secret-rotations/azure-client-secret/get-by-id", + "api-reference/endpoints/secret-rotations/azure-client-secret/get-by-name", + "api-reference/endpoints/secret-rotations/azure-client-secret/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/azure-client-secret/list", + "api-reference/endpoints/secret-rotations/azure-client-secret/rotate-secrets", + "api-reference/endpoints/secret-rotations/azure-client-secret/update" + ] + }, + { + "group": "LDAP Password", + "pages": [ + "api-reference/endpoints/secret-rotations/ldap-password/create", + "api-reference/endpoints/secret-rotations/ldap-password/delete", + "api-reference/endpoints/secret-rotations/ldap-password/get-by-id", + "api-reference/endpoints/secret-rotations/ldap-password/get-by-name", + "api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/ldap-password/list", + "api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets", + "api-reference/endpoints/secret-rotations/ldap-password/update" + ] + }, + { + "group": "Microsoft SQL Server Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/mssql-credentials/create", + "api-reference/endpoints/secret-rotations/mssql-credentials/delete", + "api-reference/endpoints/secret-rotations/mssql-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/mssql-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/mssql-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/mssql-credentials/list", + "api-reference/endpoints/secret-rotations/mssql-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/mssql-credentials/update" + ] + }, + { + "group": "MySQL Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/mysql-credentials/create", + "api-reference/endpoints/secret-rotations/mysql-credentials/delete", + "api-reference/endpoints/secret-rotations/mysql-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/mysql-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/mysql-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/mysql-credentials/list", + "api-reference/endpoints/secret-rotations/mysql-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/mysql-credentials/update" + ] + }, + { + "group": "OracleDB Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/oracledb-credentials/create", + "api-reference/endpoints/secret-rotations/oracledb-credentials/delete", + "api-reference/endpoints/secret-rotations/oracledb-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/oracledb-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/oracledb-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/oracledb-credentials/list", + "api-reference/endpoints/secret-rotations/oracledb-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/oracledb-credentials/update" + ] + }, + { + "group": "PostgreSQL Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/postgres-credentials/create", + "api-reference/endpoints/secret-rotations/postgres-credentials/delete", + "api-reference/endpoints/secret-rotations/postgres-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/postgres-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/postgres-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/postgres-credentials/list", + "api-reference/endpoints/secret-rotations/postgres-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/postgres-credentials/update" + ] + } + ] + }, + { + "group": "Secret Scanning", + "pages": [ + { + "group": "Data Sources", + "pages": [ + "api-reference/endpoints/secret-scanning/data-sources/list", + "api-reference/endpoints/secret-scanning/data-sources/options", + { + "group": "GitHub", + "pages": [ + "api-reference/endpoints/secret-scanning/data-sources/github/list", + "api-reference/endpoints/secret-scanning/data-sources/github/get-by-id", + "api-reference/endpoints/secret-scanning/data-sources/github/get-by-name", + "api-reference/endpoints/secret-scanning/data-sources/github/list-resources", + "api-reference/endpoints/secret-scanning/data-sources/github/list-scans", + "api-reference/endpoints/secret-scanning/data-sources/github/create", + "api-reference/endpoints/secret-scanning/data-sources/github/update", + "api-reference/endpoints/secret-scanning/data-sources/github/delete", + "api-reference/endpoints/secret-scanning/data-sources/github/scan", + "api-reference/endpoints/secret-scanning/data-sources/github/scan-resource" + ] + } + ] + }, + { + "group": "Findings", + "pages": [ + "api-reference/endpoints/secret-scanning/findings/list", + "api-reference/endpoints/secret-scanning/findings/update" + ] + }, + { + "group": "Configuration", + "pages": [ + "api-reference/endpoints/secret-scanning/config/get-by-project-id", + "api-reference/endpoints/secret-scanning/config/update" + ] + } + ] + }, + { + "group": "Identity Specific Privilege", + "pages": [ + { + "group": "V1 (Legacy)", + "pages": [ + "api-reference/endpoints/identity-specific-privilege/v1/create-permanent", + "api-reference/endpoints/identity-specific-privilege/v1/create-temporary", + "api-reference/endpoints/identity-specific-privilege/v1/update", + "api-reference/endpoints/identity-specific-privilege/v1/delete", + "api-reference/endpoints/identity-specific-privilege/v1/find-by-slug", + "api-reference/endpoints/identity-specific-privilege/v1/list" + ] + }, + { + "group": "V2", + "pages": [ + "api-reference/endpoints/identity-specific-privilege/v2/create", + "api-reference/endpoints/identity-specific-privilege/v2/update", + "api-reference/endpoints/identity-specific-privilege/v2/delete", + "api-reference/endpoints/identity-specific-privilege/v2/list", + "api-reference/endpoints/identity-specific-privilege/v2/find-by-id", + "api-reference/endpoints/identity-specific-privilege/v2/find-by-slug" + ] + } + ] + }, + { + "group": "App Connections", + "pages": [ + "api-reference/endpoints/app-connections/list", + "api-reference/endpoints/app-connections/options", + { + "group": "1Password", + "pages": [ + "api-reference/endpoints/app-connections/1password/list", + "api-reference/endpoints/app-connections/1password/available", + "api-reference/endpoints/app-connections/1password/get-by-id", + "api-reference/endpoints/app-connections/1password/get-by-name", + "api-reference/endpoints/app-connections/1password/create", + "api-reference/endpoints/app-connections/1password/update", + "api-reference/endpoints/app-connections/1password/delete" + ] + }, + { + "group": "Auth0", + "pages": [ + "api-reference/endpoints/app-connections/auth0/list", + "api-reference/endpoints/app-connections/auth0/available", + "api-reference/endpoints/app-connections/auth0/get-by-id", + "api-reference/endpoints/app-connections/auth0/get-by-name", + "api-reference/endpoints/app-connections/auth0/create", + "api-reference/endpoints/app-connections/auth0/update", + "api-reference/endpoints/app-connections/auth0/delete" + ] + }, + { + "group": "AWS", + "pages": [ + "api-reference/endpoints/app-connections/aws/list", + "api-reference/endpoints/app-connections/aws/available", + "api-reference/endpoints/app-connections/aws/get-by-id", + "api-reference/endpoints/app-connections/aws/get-by-name", + "api-reference/endpoints/app-connections/aws/create", + "api-reference/endpoints/app-connections/aws/update", + "api-reference/endpoints/app-connections/aws/delete" + ] + }, + { + "group": "Azure App Configuration", + "pages": [ + "api-reference/endpoints/app-connections/azure-app-configuration/list", + "api-reference/endpoints/app-connections/azure-app-configuration/available", + "api-reference/endpoints/app-connections/azure-app-configuration/get-by-id", + "api-reference/endpoints/app-connections/azure-app-configuration/get-by-name", + "api-reference/endpoints/app-connections/azure-app-configuration/create", + "api-reference/endpoints/app-connections/azure-app-configuration/update", + "api-reference/endpoints/app-connections/azure-app-configuration/delete" + ] + }, + { + "group": "Azure Client Secret", + "pages": [ + "api-reference/endpoints/app-connections/azure-client-secret/list", + "api-reference/endpoints/app-connections/azure-client-secret/available", + "api-reference/endpoints/app-connections/azure-client-secret/get-by-id", + "api-reference/endpoints/app-connections/azure-client-secret/get-by-name", + "api-reference/endpoints/app-connections/azure-client-secret/create", + "api-reference/endpoints/app-connections/azure-client-secret/update", + "api-reference/endpoints/app-connections/azure-client-secret/delete" + ] + }, + { + "group": "Azure DevOps", + "pages": [ + "api-reference/endpoints/app-connections/azure-devops/list", + "api-reference/endpoints/app-connections/azure-devops/available", + "api-reference/endpoints/app-connections/azure-devops/get-by-id", + "api-reference/endpoints/app-connections/azure-devops/get-by-name", + "api-reference/endpoints/app-connections/azure-devops/create", + "api-reference/endpoints/app-connections/azure-devops/update", + "api-reference/endpoints/app-connections/azure-devops/delete" + ] + }, + { + "group": "Azure Key Vault", + "pages": [ + "api-reference/endpoints/app-connections/azure-key-vault/list", + "api-reference/endpoints/app-connections/azure-key-vault/available", + "api-reference/endpoints/app-connections/azure-key-vault/get-by-id", + "api-reference/endpoints/app-connections/azure-key-vault/get-by-name", + "api-reference/endpoints/app-connections/azure-key-vault/create", + "api-reference/endpoints/app-connections/azure-key-vault/update", + "api-reference/endpoints/app-connections/azure-key-vault/delete" + ] + }, + { + "group": "Camunda", + "pages": [ + "api-reference/endpoints/app-connections/camunda/list", + "api-reference/endpoints/app-connections/camunda/available", + "api-reference/endpoints/app-connections/camunda/get-by-id", + "api-reference/endpoints/app-connections/camunda/get-by-name", + "api-reference/endpoints/app-connections/camunda/create", + "api-reference/endpoints/app-connections/camunda/update", + "api-reference/endpoints/app-connections/camunda/delete" + ] + }, + { + "group": "Cloudflare", + "pages": [ + "api-reference/endpoints/app-connections/cloudflare/list", + "api-reference/endpoints/app-connections/cloudflare/available", + "api-reference/endpoints/app-connections/cloudflare/get-by-id", + "api-reference/endpoints/app-connections/cloudflare/get-by-name", + "api-reference/endpoints/app-connections/cloudflare/create", + "api-reference/endpoints/app-connections/cloudflare/update", + "api-reference/endpoints/app-connections/cloudflare/delete" + ] + }, + { + "group": "Databricks", + "pages": [ + "api-reference/endpoints/app-connections/databricks/list", + "api-reference/endpoints/app-connections/databricks/available", + "api-reference/endpoints/app-connections/databricks/get-by-id", + "api-reference/endpoints/app-connections/databricks/get-by-name", + "api-reference/endpoints/app-connections/databricks/create", + "api-reference/endpoints/app-connections/databricks/update", + "api-reference/endpoints/app-connections/databricks/delete" + ] + }, + { + "group": "Fly.io", + "pages": [ + "api-reference/endpoints/app-connections/flyio/list", + "api-reference/endpoints/app-connections/flyio/available", + "api-reference/endpoints/app-connections/flyio/get-by-id", + "api-reference/endpoints/app-connections/flyio/get-by-name", + "api-reference/endpoints/app-connections/flyio/create", + "api-reference/endpoints/app-connections/flyio/update", + "api-reference/endpoints/app-connections/flyio/delete" + ] + }, + { + "group": "GCP", + "pages": [ + "api-reference/endpoints/app-connections/gcp/list", + "api-reference/endpoints/app-connections/gcp/available", + "api-reference/endpoints/app-connections/gcp/get-by-id", + "api-reference/endpoints/app-connections/gcp/get-by-name", + "api-reference/endpoints/app-connections/gcp/create", + "api-reference/endpoints/app-connections/gcp/update", + "api-reference/endpoints/app-connections/gcp/delete" + ] + }, + { + "group": "GitHub", + "pages": [ + "api-reference/endpoints/app-connections/github/list", + "api-reference/endpoints/app-connections/github/available", + "api-reference/endpoints/app-connections/github/get-by-id", + "api-reference/endpoints/app-connections/github/get-by-name", + "api-reference/endpoints/app-connections/github/create", + "api-reference/endpoints/app-connections/github/update", + "api-reference/endpoints/app-connections/github/delete" + ] + }, + { + "group": "GitLab", + "pages": [ + "api-reference/endpoints/app-connections/gitlab/list", + "api-reference/endpoints/app-connections/gitlab/available", + "api-reference/endpoints/app-connections/gitlab/get-by-id", + "api-reference/endpoints/app-connections/gitlab/get-by-name", + "api-reference/endpoints/app-connections/gitlab/create", + "api-reference/endpoints/app-connections/gitlab/update", + "api-reference/endpoints/app-connections/gitlab/delete" + ] + }, + { + "group": "GitHub Radar", + "pages": [ + "api-reference/endpoints/app-connections/github-radar/list", + "api-reference/endpoints/app-connections/github-radar/available", + "api-reference/endpoints/app-connections/github-radar/get-by-id", + "api-reference/endpoints/app-connections/github-radar/get-by-name", + "api-reference/endpoints/app-connections/github-radar/create", + "api-reference/endpoints/app-connections/github-radar/update", + "api-reference/endpoints/app-connections/github-radar/delete" + ] + }, + { + "group": "Hashicorp Vault", + "pages": [ + "api-reference/endpoints/app-connections/hashicorp-vault/list", + "api-reference/endpoints/app-connections/hashicorp-vault/available", + "api-reference/endpoints/app-connections/hashicorp-vault/get-by-id", + "api-reference/endpoints/app-connections/hashicorp-vault/get-by-name", + "api-reference/endpoints/app-connections/hashicorp-vault/create", + "api-reference/endpoints/app-connections/hashicorp-vault/update", + "api-reference/endpoints/app-connections/hashicorp-vault/delete" + ] + }, + { + "group": "Heroku", + "pages": [ + "api-reference/endpoints/app-connections/heroku/list", + "api-reference/endpoints/app-connections/heroku/available", + "api-reference/endpoints/app-connections/heroku/get-by-id", + "api-reference/endpoints/app-connections/heroku/get-by-name", + "api-reference/endpoints/app-connections/heroku/create", + "api-reference/endpoints/app-connections/heroku/update", + "api-reference/endpoints/app-connections/heroku/delete" + ] + }, + { + "group": "Humanitec", + "pages": [ + "api-reference/endpoints/app-connections/humanitec/list", + "api-reference/endpoints/app-connections/humanitec/available", + "api-reference/endpoints/app-connections/humanitec/get-by-id", + "api-reference/endpoints/app-connections/humanitec/get-by-name", + "api-reference/endpoints/app-connections/humanitec/create", + "api-reference/endpoints/app-connections/humanitec/update", + "api-reference/endpoints/app-connections/humanitec/delete" + ] + }, + { + "group": "LDAP", + "pages": [ + "api-reference/endpoints/app-connections/ldap/list", + "api-reference/endpoints/app-connections/ldap/available", + "api-reference/endpoints/app-connections/ldap/get-by-id", + "api-reference/endpoints/app-connections/ldap/get-by-name", + "api-reference/endpoints/app-connections/ldap/create", + "api-reference/endpoints/app-connections/ldap/update", + "api-reference/endpoints/app-connections/ldap/delete" + ] + }, + { + "group": "Microsoft SQL Server", + "pages": [ + "api-reference/endpoints/app-connections/mssql/list", + "api-reference/endpoints/app-connections/mssql/available", + "api-reference/endpoints/app-connections/mssql/get-by-id", + "api-reference/endpoints/app-connections/mssql/get-by-name", + "api-reference/endpoints/app-connections/mssql/create", + "api-reference/endpoints/app-connections/mssql/update", + "api-reference/endpoints/app-connections/mssql/delete" + ] + }, + { + "group": "MySQL", + "pages": [ + "api-reference/endpoints/app-connections/mysql/list", + "api-reference/endpoints/app-connections/mysql/available", + "api-reference/endpoints/app-connections/mysql/get-by-id", + "api-reference/endpoints/app-connections/mysql/get-by-name", + "api-reference/endpoints/app-connections/mysql/create", + "api-reference/endpoints/app-connections/mysql/update", + "api-reference/endpoints/app-connections/mysql/delete" + ] + }, + { + "group": "OCI", + "pages": [ + "api-reference/endpoints/app-connections/oci/list", + "api-reference/endpoints/app-connections/oci/available", + "api-reference/endpoints/app-connections/oci/get-by-id", + "api-reference/endpoints/app-connections/oci/get-by-name", + "api-reference/endpoints/app-connections/oci/create", + "api-reference/endpoints/app-connections/oci/update", + "api-reference/endpoints/app-connections/oci/delete" + ] + }, + { + "group": "OracleDB", + "pages": [ + "api-reference/endpoints/app-connections/oracledb/list", + "api-reference/endpoints/app-connections/oracledb/available", + "api-reference/endpoints/app-connections/oracledb/get-by-id", + "api-reference/endpoints/app-connections/oracledb/get-by-name", + "api-reference/endpoints/app-connections/oracledb/create", + "api-reference/endpoints/app-connections/oracledb/update", + "api-reference/endpoints/app-connections/oracledb/delete" + ] + }, + { + "group": "PostgreSQL", + "pages": [ + "api-reference/endpoints/app-connections/postgres/list", + "api-reference/endpoints/app-connections/postgres/available", + "api-reference/endpoints/app-connections/postgres/get-by-id", + "api-reference/endpoints/app-connections/postgres/get-by-name", + "api-reference/endpoints/app-connections/postgres/create", + "api-reference/endpoints/app-connections/postgres/update", + "api-reference/endpoints/app-connections/postgres/delete" + ] + }, + { + "group": "Render", + "pages": [ + "api-reference/endpoints/app-connections/render/list", + "api-reference/endpoints/app-connections/render/available", + "api-reference/endpoints/app-connections/render/get-by-id", + "api-reference/endpoints/app-connections/render/get-by-name", + "api-reference/endpoints/app-connections/render/create", + "api-reference/endpoints/app-connections/render/update", + "api-reference/endpoints/app-connections/render/delete" + ] + }, + { + "group": "TeamCity", + "pages": [ + "api-reference/endpoints/app-connections/teamcity/list", + "api-reference/endpoints/app-connections/teamcity/available", + "api-reference/endpoints/app-connections/teamcity/get-by-id", + "api-reference/endpoints/app-connections/teamcity/get-by-name", + "api-reference/endpoints/app-connections/teamcity/create", + "api-reference/endpoints/app-connections/teamcity/update", + "api-reference/endpoints/app-connections/teamcity/delete" + ] + }, + { + "group": "Terraform Cloud", + "pages": [ + "api-reference/endpoints/app-connections/terraform-cloud/list", + "api-reference/endpoints/app-connections/terraform-cloud/available", + "api-reference/endpoints/app-connections/terraform-cloud/get-by-id", + "api-reference/endpoints/app-connections/terraform-cloud/get-by-name", + "api-reference/endpoints/app-connections/terraform-cloud/create", + "api-reference/endpoints/app-connections/terraform-cloud/update", + "api-reference/endpoints/app-connections/terraform-cloud/delete" + ] + }, + { + "group": "Vercel", + "pages": [ + "api-reference/endpoints/app-connections/vercel/list", + "api-reference/endpoints/app-connections/vercel/available", + "api-reference/endpoints/app-connections/vercel/get-by-id", + "api-reference/endpoints/app-connections/vercel/get-by-name", + "api-reference/endpoints/app-connections/vercel/create", + "api-reference/endpoints/app-connections/vercel/update", + "api-reference/endpoints/app-connections/vercel/delete" + ] + }, + { + "group": "Windmill", + "pages": [ + "api-reference/endpoints/app-connections/windmill/list", + "api-reference/endpoints/app-connections/windmill/available", + "api-reference/endpoints/app-connections/windmill/get-by-id", + "api-reference/endpoints/app-connections/windmill/get-by-name", + "api-reference/endpoints/app-connections/windmill/create", + "api-reference/endpoints/app-connections/windmill/update", + "api-reference/endpoints/app-connections/windmill/delete" + ] + } + ] + }, + { + "group": "Secret Syncs", + "pages": [ + "api-reference/endpoints/secret-syncs/list", + "api-reference/endpoints/secret-syncs/options", + { + "group": "1Password", + "pages": [ + "api-reference/endpoints/secret-syncs/1password/list", + "api-reference/endpoints/secret-syncs/1password/get-by-id", + "api-reference/endpoints/secret-syncs/1password/get-by-name", + "api-reference/endpoints/secret-syncs/1password/create", + "api-reference/endpoints/secret-syncs/1password/update", + "api-reference/endpoints/secret-syncs/1password/delete", + "api-reference/endpoints/secret-syncs/1password/sync-secrets", + "api-reference/endpoints/secret-syncs/1password/import-secrets", + "api-reference/endpoints/secret-syncs/1password/remove-secrets" + ] + }, + { + "group": "AWS Parameter Store", + "pages": [ + "api-reference/endpoints/secret-syncs/aws-parameter-store/list", + "api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-id", + "api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-name", + "api-reference/endpoints/secret-syncs/aws-parameter-store/create", + "api-reference/endpoints/secret-syncs/aws-parameter-store/update", + "api-reference/endpoints/secret-syncs/aws-parameter-store/delete", + "api-reference/endpoints/secret-syncs/aws-parameter-store/sync-secrets", + "api-reference/endpoints/secret-syncs/aws-parameter-store/import-secrets", + "api-reference/endpoints/secret-syncs/aws-parameter-store/remove-secrets" + ] + }, + { + "group": "AWS Secrets Manager", + "pages": [ + "api-reference/endpoints/secret-syncs/aws-secrets-manager/list", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-id", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/get-by-name", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/create", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/update", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/delete", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/sync-secrets", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/import-secrets", + "api-reference/endpoints/secret-syncs/aws-secrets-manager/remove-secrets" + ] + }, + { + "group": "Azure App Configuration", + "pages": [ + "api-reference/endpoints/secret-syncs/azure-app-configuration/list", + "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-id", + "api-reference/endpoints/secret-syncs/azure-app-configuration/get-by-name", + "api-reference/endpoints/secret-syncs/azure-app-configuration/create", + "api-reference/endpoints/secret-syncs/azure-app-configuration/update", + "api-reference/endpoints/secret-syncs/azure-app-configuration/delete", + "api-reference/endpoints/secret-syncs/azure-app-configuration/sync-secrets", + "api-reference/endpoints/secret-syncs/azure-app-configuration/import-secrets", + "api-reference/endpoints/secret-syncs/azure-app-configuration/remove-secrets" + ] + }, + { + "group": "Azure DevOps", + "pages": [ + "api-reference/endpoints/secret-syncs/azure-devops/list", + "api-reference/endpoints/secret-syncs/azure-devops/get-by-id", + "api-reference/endpoints/secret-syncs/azure-devops/get-by-name", + "api-reference/endpoints/secret-syncs/azure-devops/create", + "api-reference/endpoints/secret-syncs/azure-devops/update", + "api-reference/endpoints/secret-syncs/azure-devops/delete", + "api-reference/endpoints/secret-syncs/azure-devops/sync-secrets", + "api-reference/endpoints/secret-syncs/azure-devops/import-secrets", + "api-reference/endpoints/secret-syncs/azure-devops/remove-secrets" + ] + }, + { + "group": "Azure Key Vault", + "pages": [ + "api-reference/endpoints/secret-syncs/azure-key-vault/list", + "api-reference/endpoints/secret-syncs/azure-key-vault/get-by-id", + "api-reference/endpoints/secret-syncs/azure-key-vault/get-by-name", + "api-reference/endpoints/secret-syncs/azure-key-vault/create", + "api-reference/endpoints/secret-syncs/azure-key-vault/update", + "api-reference/endpoints/secret-syncs/azure-key-vault/delete", + "api-reference/endpoints/secret-syncs/azure-key-vault/sync-secrets", + "api-reference/endpoints/secret-syncs/azure-key-vault/import-secrets", + "api-reference/endpoints/secret-syncs/azure-key-vault/remove-secrets" + ] + }, + { + "group": "Camunda", + "pages": [ + "api-reference/endpoints/secret-syncs/camunda/list", + "api-reference/endpoints/secret-syncs/camunda/get-by-id", + "api-reference/endpoints/secret-syncs/camunda/get-by-name", + "api-reference/endpoints/secret-syncs/camunda/create", + "api-reference/endpoints/secret-syncs/camunda/update", + "api-reference/endpoints/secret-syncs/camunda/delete", + "api-reference/endpoints/secret-syncs/camunda/sync-secrets", + "api-reference/endpoints/secret-syncs/camunda/remove-secrets" + ] + }, + { + "group": "Cloudflare Pages", + "pages": [ + "api-reference/endpoints/secret-syncs/cloudflare-pages/list", + "api-reference/endpoints/secret-syncs/cloudflare-pages/get-by-id", + "api-reference/endpoints/secret-syncs/cloudflare-pages/get-by-name", + "api-reference/endpoints/secret-syncs/cloudflare-pages/create", + "api-reference/endpoints/secret-syncs/cloudflare-pages/update", + "api-reference/endpoints/secret-syncs/cloudflare-pages/delete", + "api-reference/endpoints/secret-syncs/cloudflare-pages/sync-secrets", + "api-reference/endpoints/secret-syncs/cloudflare-pages/remove-secrets" + ] + }, + { + "group": "Databricks", + "pages": [ + "api-reference/endpoints/secret-syncs/databricks/list", + "api-reference/endpoints/secret-syncs/databricks/get-by-id", + "api-reference/endpoints/secret-syncs/databricks/get-by-name", + "api-reference/endpoints/secret-syncs/databricks/create", + "api-reference/endpoints/secret-syncs/databricks/update", + "api-reference/endpoints/secret-syncs/databricks/delete", + "api-reference/endpoints/secret-syncs/databricks/sync-secrets", + "api-reference/endpoints/secret-syncs/databricks/remove-secrets" + ] + }, + { + "group": "Fly.io", + "pages": [ + "api-reference/endpoints/secret-syncs/flyio/list", + "api-reference/endpoints/secret-syncs/flyio/get-by-id", + "api-reference/endpoints/secret-syncs/flyio/get-by-name", + "api-reference/endpoints/secret-syncs/flyio/create", + "api-reference/endpoints/secret-syncs/flyio/update", + "api-reference/endpoints/secret-syncs/flyio/delete", + "api-reference/endpoints/secret-syncs/flyio/sync-secrets", + "api-reference/endpoints/secret-syncs/flyio/remove-secrets" + ] + }, + { + "group": "GCP Secret Manager", + "pages": [ + "api-reference/endpoints/secret-syncs/gcp-secret-manager/list", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-id", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/get-by-name", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/create", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/update", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/delete", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/sync-secrets", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/import-secrets", + "api-reference/endpoints/secret-syncs/gcp-secret-manager/remove-secrets" + ] + }, + { + "group": "GitHub", + "pages": [ + "api-reference/endpoints/secret-syncs/github/list", + "api-reference/endpoints/secret-syncs/github/get-by-id", + "api-reference/endpoints/secret-syncs/github/get-by-name", + "api-reference/endpoints/secret-syncs/github/create", + "api-reference/endpoints/secret-syncs/github/update", + "api-reference/endpoints/secret-syncs/github/delete", + "api-reference/endpoints/secret-syncs/github/sync-secrets", + "api-reference/endpoints/secret-syncs/github/remove-secrets" + ] + }, + { + "group": "GitLab", + "pages": [ + "api-reference/endpoints/secret-syncs/gitlab/list", + "api-reference/endpoints/secret-syncs/gitlab/get-by-id", + "api-reference/endpoints/secret-syncs/gitlab/get-by-name", + "api-reference/endpoints/secret-syncs/gitlab/create", + "api-reference/endpoints/secret-syncs/gitlab/update", + "api-reference/endpoints/secret-syncs/gitlab/delete", + "api-reference/endpoints/secret-syncs/gitlab/sync-secrets", + "api-reference/endpoints/secret-syncs/gitlab/remove-secrets" + ] + }, + { + "group": "Hashicorp Vault", + "pages": [ + "api-reference/endpoints/secret-syncs/hashicorp-vault/list", + "api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-id", + "api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-name", + "api-reference/endpoints/secret-syncs/hashicorp-vault/create", + "api-reference/endpoints/secret-syncs/hashicorp-vault/update", + "api-reference/endpoints/secret-syncs/hashicorp-vault/delete", + "api-reference/endpoints/secret-syncs/hashicorp-vault/sync-secrets", + "api-reference/endpoints/secret-syncs/hashicorp-vault/import-secrets", + "api-reference/endpoints/secret-syncs/hashicorp-vault/remove-secrets" + ] + }, + { + "group": "Heroku", + "pages": [ + "api-reference/endpoints/secret-syncs/heroku/list", + "api-reference/endpoints/secret-syncs/heroku/get-by-id", + "api-reference/endpoints/secret-syncs/heroku/get-by-name", + "api-reference/endpoints/secret-syncs/heroku/create", + "api-reference/endpoints/secret-syncs/heroku/update", + "api-reference/endpoints/secret-syncs/heroku/delete", + "api-reference/endpoints/secret-syncs/heroku/sync-secrets", + "api-reference/endpoints/secret-syncs/heroku/remove-secrets" + ] + }, + { + "group": "Humanitec", + "pages": [ + "api-reference/endpoints/secret-syncs/humanitec/list", + "api-reference/endpoints/secret-syncs/humanitec/get-by-id", + "api-reference/endpoints/secret-syncs/humanitec/get-by-name", + "api-reference/endpoints/secret-syncs/humanitec/create", + "api-reference/endpoints/secret-syncs/humanitec/update", + "api-reference/endpoints/secret-syncs/humanitec/delete", + "api-reference/endpoints/secret-syncs/humanitec/sync-secrets", + "api-reference/endpoints/secret-syncs/humanitec/remove-secrets" + ] + }, + { + "group": "OCI", + "pages": [ + "api-reference/endpoints/secret-syncs/oci-vault/list", + "api-reference/endpoints/secret-syncs/oci-vault/get-by-id", + "api-reference/endpoints/secret-syncs/oci-vault/get-by-name", + "api-reference/endpoints/secret-syncs/oci-vault/create", + "api-reference/endpoints/secret-syncs/oci-vault/update", + "api-reference/endpoints/secret-syncs/oci-vault/delete", + "api-reference/endpoints/secret-syncs/oci-vault/sync-secrets", + "api-reference/endpoints/secret-syncs/oci-vault/import-secrets", + "api-reference/endpoints/secret-syncs/oci-vault/remove-secrets" + ] + }, + { + "group": "Render", + "pages": [ + "api-reference/endpoints/secret-syncs/render/list", + "api-reference/endpoints/secret-syncs/render/get-by-id", + "api-reference/endpoints/secret-syncs/render/get-by-name", + "api-reference/endpoints/secret-syncs/render/create", + "api-reference/endpoints/secret-syncs/render/update", + "api-reference/endpoints/secret-syncs/render/delete", + "api-reference/endpoints/secret-syncs/render/sync-secrets", + "api-reference/endpoints/secret-syncs/render/import-secrets", + "api-reference/endpoints/secret-syncs/render/remove-secrets" + ] + }, + { + "group": "TeamCity", + "pages": [ + "api-reference/endpoints/secret-syncs/teamcity/list", + "api-reference/endpoints/secret-syncs/teamcity/get-by-id", + "api-reference/endpoints/secret-syncs/teamcity/get-by-name", + "api-reference/endpoints/secret-syncs/teamcity/create", + "api-reference/endpoints/secret-syncs/teamcity/update", + "api-reference/endpoints/secret-syncs/teamcity/delete", + "api-reference/endpoints/secret-syncs/teamcity/sync-secrets", + "api-reference/endpoints/secret-syncs/teamcity/import-secrets", + "api-reference/endpoints/secret-syncs/teamcity/remove-secrets" + ] + }, + { + "group": "Terraform Cloud", + "pages": [ + "api-reference/endpoints/secret-syncs/terraform-cloud/list", + "api-reference/endpoints/secret-syncs/terraform-cloud/get-by-id", + "api-reference/endpoints/secret-syncs/terraform-cloud/get-by-name", + "api-reference/endpoints/secret-syncs/terraform-cloud/create", + "api-reference/endpoints/secret-syncs/terraform-cloud/update", + "api-reference/endpoints/secret-syncs/terraform-cloud/delete", + "api-reference/endpoints/secret-syncs/terraform-cloud/sync-secrets", + "api-reference/endpoints/secret-syncs/terraform-cloud/remove-secrets" + ] + }, + { + "group": "Vercel", + "pages": [ + "api-reference/endpoints/secret-syncs/vercel/list", + "api-reference/endpoints/secret-syncs/vercel/get-by-id", + "api-reference/endpoints/secret-syncs/vercel/get-by-name", + "api-reference/endpoints/secret-syncs/vercel/create", + "api-reference/endpoints/secret-syncs/vercel/update", + "api-reference/endpoints/secret-syncs/vercel/delete", + "api-reference/endpoints/secret-syncs/vercel/sync-secrets", + "api-reference/endpoints/secret-syncs/vercel/import-secrets", + "api-reference/endpoints/secret-syncs/vercel/remove-secrets" + ] + }, + { + "group": "Windmill", + "pages": [ + "api-reference/endpoints/secret-syncs/windmill/list", + "api-reference/endpoints/secret-syncs/windmill/get-by-id", + "api-reference/endpoints/secret-syncs/windmill/get-by-name", + "api-reference/endpoints/secret-syncs/windmill/create", + "api-reference/endpoints/secret-syncs/windmill/update", + "api-reference/endpoints/secret-syncs/windmill/delete", + "api-reference/endpoints/secret-syncs/windmill/sync-secrets", + "api-reference/endpoints/secret-syncs/windmill/import-secrets", + "api-reference/endpoints/secret-syncs/windmill/remove-secrets" + ] + } + ] + }, + { + "group": "Integrations", + "pages": [ + "api-reference/endpoints/integrations/create-auth", + "api-reference/endpoints/integrations/list-auth", + "api-reference/endpoints/integrations/find-auth", + "api-reference/endpoints/integrations/delete-auth", + "api-reference/endpoints/integrations/delete-auth-by-id", + "api-reference/endpoints/integrations/create", + "api-reference/endpoints/integrations/update", + "api-reference/endpoints/integrations/delete", + "api-reference/endpoints/integrations/list-project-integrations" + ] + }, + { + "group": "Service Tokens", + "pages": ["api-reference/endpoints/service-tokens/get"] + }, + { + "group": "Audit Logs", + "pages": ["api-reference/endpoints/audit-logs/export-audit-log"] + } + ] + }, + { + "group": "Infisical PKI", + "pages": [ + { + "group": "Subscribers", + "pages": [ + "api-reference/endpoints/pki/subscribers/list-certs", + "api-reference/endpoints/pki/subscribers/create", + "api-reference/endpoints/pki/subscribers/read", + "api-reference/endpoints/pki/subscribers/update", + "api-reference/endpoints/pki/subscribers/delete", + "api-reference/endpoints/pki/subscribers/issue-cert", + "api-reference/endpoints/pki/subscribers/sign-cert", + "api-reference/endpoints/pki/subscribers/order-cert", + "api-reference/endpoints/pki/subscribers/get-latest-cert-bundle" + ] + }, + { + "group": "Certificate Authorities", + "pages": [ + { + "group": "ACME", + "pages": [ + "api-reference/endpoints/certificate-authorities/acme/list", + "api-reference/endpoints/certificate-authorities/acme/create", + "api-reference/endpoints/certificate-authorities/acme/read", + "api-reference/endpoints/certificate-authorities/acme/update", + "api-reference/endpoints/certificate-authorities/acme/delete" + ] + }, + { + "group": "Internal", + "pages": [ + "api-reference/endpoints/certificate-authorities/internal/list", + "api-reference/endpoints/certificate-authorities/internal/create", + "api-reference/endpoints/certificate-authorities/internal/read", + "api-reference/endpoints/certificate-authorities/internal/update", + "api-reference/endpoints/certificate-authorities/internal/delete" + ] + }, + "api-reference/endpoints/certificate-authorities/list", + "api-reference/endpoints/certificate-authorities/create", + "api-reference/endpoints/certificate-authorities/read", + "api-reference/endpoints/certificate-authorities/update", + "api-reference/endpoints/certificate-authorities/delete", + "api-reference/endpoints/certificate-authorities/renew", + "api-reference/endpoints/certificate-authorities/list-ca-certs", + "api-reference/endpoints/certificate-authorities/csr", + "api-reference/endpoints/certificate-authorities/cert", + "api-reference/endpoints/certificate-authorities/sign-intermediate", + "api-reference/endpoints/certificate-authorities/import-cert", + "api-reference/endpoints/certificate-authorities/issue-cert", + "api-reference/endpoints/certificate-authorities/sign-cert", + "api-reference/endpoints/certificate-authorities/crl" + ] + }, + { + "group": "Certificates", + "pages": [ + "api-reference/endpoints/certificates/list", + "api-reference/endpoints/certificates/read", + "api-reference/endpoints/certificates/revoke", + "api-reference/endpoints/certificates/delete", + "api-reference/endpoints/certificates/cert-body", + "api-reference/endpoints/certificates/bundle", + "api-reference/endpoints/certificates/private-key", + "api-reference/endpoints/certificates/issue-certificate", + "api-reference/endpoints/certificates/sign-certificate" + ] + }, + { + "group": "Certificate Templates", + "pages": [ + "api-reference/endpoints/certificate-templates/create", + "api-reference/endpoints/certificate-templates/update", + "api-reference/endpoints/certificate-templates/get-by-id", + "api-reference/endpoints/certificate-templates/delete" + ] + }, + { + "group": "Certificate Collections", + "pages": [ + "api-reference/endpoints/pki-collections/create", + "api-reference/endpoints/pki-collections/read", + "api-reference/endpoints/pki-collections/update", + "api-reference/endpoints/pki-collections/delete", + "api-reference/endpoints/pki-collections/add-item", + "api-reference/endpoints/pki-collections/list-items", + "api-reference/endpoints/pki-collections/delete-item" + ] + }, + { + "group": "PKI Alerting", + "pages": [ + "api-reference/endpoints/pki-alerts/create", + "api-reference/endpoints/pki-alerts/read", + "api-reference/endpoints/pki-alerts/update", + "api-reference/endpoints/pki-alerts/delete" + ] + } + ] + }, + { + "group": "Infisical SSH", + "pages": [ + { + "group": "Hosts", + "pages": [ + "api-reference/endpoints/ssh/hosts/list-my", + "api-reference/endpoints/ssh/hosts/list", + "api-reference/endpoints/ssh/hosts/create", + "api-reference/endpoints/ssh/hosts/read", + "api-reference/endpoints/ssh/hosts/update", + "api-reference/endpoints/ssh/hosts/delete", + "api-reference/endpoints/ssh/hosts/issue-host-cert", + "api-reference/endpoints/ssh/hosts/issue-user-cert", + "api-reference/endpoints/ssh/hosts/read-user-ca-pk", + "api-reference/endpoints/ssh/hosts/read-host-ca-pk" + ] + }, + { + "group": "Host Groups", + "pages": [ + "api-reference/endpoints/ssh/groups/list", + "api-reference/endpoints/ssh/groups/create", + "api-reference/endpoints/ssh/groups/read", + "api-reference/endpoints/ssh/groups/update", + "api-reference/endpoints/ssh/groups/delete", + "api-reference/endpoints/ssh/groups/add-host", + "api-reference/endpoints/ssh/groups/list-hosts", + "api-reference/endpoints/ssh/groups/remove-host" + ] + }, + { + "group": "Certificates", + "pages": [ + "api-reference/endpoints/ssh/certificates/issue-credentials", + "api-reference/endpoints/ssh/certificates/sign-key" + ] + }, + { + "group": "Certificate Authorities", + "pages": [ + "api-reference/endpoints/ssh/ca/list", + "api-reference/endpoints/ssh/ca/create", + "api-reference/endpoints/ssh/ca/read", + "api-reference/endpoints/ssh/ca/update", + "api-reference/endpoints/ssh/ca/delete", + "api-reference/endpoints/ssh/ca/public-key", + "api-reference/endpoints/ssh/ca/list-certificate-templates" + ] + }, + { + "group": "Certificate Templates", + "pages": [ + "api-reference/endpoints/ssh/certificate-templates/list", + "api-reference/endpoints/ssh/certificate-templates/create", + "api-reference/endpoints/ssh/certificate-templates/read", + "api-reference/endpoints/ssh/certificate-templates/update", + "api-reference/endpoints/ssh/certificate-templates/delete" + ] + } + ] + }, + { + "group": "Infisical KMS", + "pages": [ + { + "group": "Keys", + "pages": [ + "api-reference/endpoints/kms/keys/list", + "api-reference/endpoints/kms/keys/get-by-id", + "api-reference/endpoints/kms/keys/get-by-name", + "api-reference/endpoints/kms/keys/create", + "api-reference/endpoints/kms/keys/update", + "api-reference/endpoints/kms/keys/delete" + ] + }, + { + "group": "Encryption", + "pages": [ + "api-reference/endpoints/kms/encryption/encrypt", + "api-reference/endpoints/kms/encryption/decrypt" + ] + }, + { + "group": "Signing", + "pages": [ + "api-reference/endpoints/kms/signing/sign", + "api-reference/endpoints/kms/signing/verify", + "api-reference/endpoints/kms/signing/public-key", + "api-reference/endpoints/kms/signing/signing-algorithms" + ] + } + ] + } + ] + }, + { + "tab": "SDKs", + "groups": [ + { + "group": "", + "pages": ["sdks/overview"] + }, + { + "group": "SDK's", + "pages": [ + "sdks/languages/node", + "sdks/languages/python", + "sdks/languages/java", + "sdks/languages/csharp", + "sdks/languages/go", + "sdks/languages/ruby" + ] + } + ] + }, + { + "tab": "Changelog", + "groups": [ + { + "group": "", + "pages": ["changelog/overview"] + } + ] + } + ] + }, + "logo": { + "light": "/logo/light.svg", + "dark": "/logo/dark.svg", + "href": "https://infisical.com" + }, + "api": { + "openapi": "https://app.infisical.com/api/docs/json", + "mdx": { + "server": ["https://app.infisical.com", "http://localhost:8080"] + } + }, + "appearance": { + "default": "light", + "strict": true + }, + "background": { + "color": { + "light": "#ffffff", + "dark": "#0D1117" + } + }, + "navbar": { + "links": [ + { + "label": "Log In", + "href": "https://app.infisical.com/login" + } + ], + "primary": { + "type": "button", + "label": "Start for Free", + "href": "https://app.infisical.com/signup" + } + }, + "footer": { + "socials": { + "x": "https://www.twitter.com/infisical/", + "linkedin": "https://www.linkedin.com/company/infisical/", + "github": "https://github.com/Infisical/infisical-cli", + "slack": "https://infisical.com/slack" + }, + "links": [ + { + "header": "PRODUCT", + "items": [ + { + "label": "Secret Management", + "href": "https://infisical.com/" + }, + { + "label": "Secret Scanning", + "href": "https://infisical.com/radar" + }, + { + "label": "Share Secrets", + "href": "https://app.infisical.com/share-secret" + }, + { + "label": "Pricing", + "href": "https://infisical.com/pricing" + }, + { + "label": "Security", + "href": "https://infisical.com/docs/internals/security" + }, + { + "label": "Blog", + "href": "https://infisical.com/blog" + }, + { + "label": "Infisical vs Vault", + "href": "https://infisical.com/infisical-vs-hashicorp-vault" + }, + { + "label": "Forum", + "href": "https://questions.infisical.com/" + } + ] + }, + { + "header": "USE CASES", + "items": [ + { + "label": "Infisical Agent", + "href": "https://infisical.com/docs/documentation/getting-started/introduction" + }, + { + "label": "Kubernetes", + "href": "https://infisical.com/docs/integrations/platforms/kubernetes" + }, + { + "label": "Dynamic Secrets", + "href": "https://infisical.com/docs/documentation/platform/dynamic-secrets/overview" + }, + { + "label": "Terraform", + "href": "https://infisical.com/docs/integrations/frameworks/terraform" + }, + { + "label": "Ansible", + "href": "https://infisical.com/docs/integrations/platforms/ansible" + }, + { + "label": "Jenkins", + "href": "https://infisical.com/docs/integrations/cicd/jenkins" + }, + { + "label": "Docker", + "href": "https://infisical.com/docs/integrations/platforms/docker-intro" + }, + { + "label": "AWS ECS", + "href": "https://infisical.com/docs/integrations/platforms/ecs-with-agent" + }, + { + "label": "GitLab", + "href": "https://infisical.com/docs/integrations/cicd/gitlab" + }, + { + "label": "GitHub", + "href": "https://infisical.com/docs/integrations/cicd/githubactions" + }, + { + "label": "SDK", + "href": "https://infisical.com/docs/sdks/overview" + } + ] + }, + { + "header": "DEVELOPERS", + "items": [ + { + "label": "Changelog", + "href": "https://www.infisical.com/docs/changelog" + }, + { + "label": "Status", + "href": "https://status.infisical.com/" + }, + { + "label": "Feedback & Requests", + "href": "https://github.com/Infisical/infisical/issues" + }, + { + "label": "Trust of Center", + "href": "https://app.vanta.com/infisical.com/trust/hoop8cr78cuarxo9sztvs" + }, + { + "label": "Open Source Friends", + "href": "https://infisical.com/infisical-friends" + }, + { + "label": "How to contribute", + "href": "https://www.infisical.com/infisical-heroes" + } + ] + }, + { + "header": "OTHERS", + "items": [ + { + "label": "Customers", + "href": "https://infisical.com/customers/traba" + }, + { + "label": "Company Handbook", + "href": "https://infisical.com/wiki/handbook/overview" + }, + { + "label": "Careers", + "href": "https://infisical.com/careers" + }, + { + "label": "Terms of Service", + "href": "https://infisical.com/terms" + }, + { + "label": "Privacy Policy", + "href": "https://infisical.com/privacy" + }, + { + "label": "Subprocessors", + "href": "https://infisical.com/subprocessors" + }, + { + "label": "SLA", + "href": "https://infisical.com/sla" + }, + { + "label": "Team Email", + "href": "mailto:team@infisical.com" + }, + { + "label": "Sales", + "href": "mailto:sales@infisical.com" + }, + { + "label": "Support", + "href": "https://infisical.com/slack" + } + ] + } + ] + }, + "integrations": { + "koala": { + "publicApiKey": "pk_b50d7184e0e39ddd5cdb43cf6abeadd9b97d" + } + } +} diff --git a/docs/documentation/guides/nextjs-vercel.mdx b/docs/documentation/guides/nextjs-vercel.mdx index 5aeadc752..f4dd9ceca 100644 --- a/docs/documentation/guides/nextjs-vercel.mdx +++ b/docs/documentation/guides/nextjs-vercel.mdx @@ -127,8 +127,8 @@ Follow the instructions for your operating system to install the Infisical CLI. - 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. 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 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). diff --git a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx index a02d80a5c..03bc5df84 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx @@ -161,6 +161,11 @@ Replace **\** with your AWS account id and **\** w {{replace identity.name 'user' 'replace'}} // testreplace ``` + + + Tags to be added to the created IAM User resource. + + Select *Assume Role* method. @@ -304,6 +309,10 @@ Replace **\** with your AWS account id and **\** w - `{{unixTimestamp}}`: Current Unix timestamp + + Tags to be added to the created IAM User resource. + + diff --git a/docs/documentation/platform/dynamic-secrets/github.mdx b/docs/documentation/platform/dynamic-secrets/github.mdx new file mode 100644 index 000000000..b1ceb9871 --- /dev/null +++ b/docs/documentation/platform/dynamic-secrets/github.mdx @@ -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 + + + + 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) + + + If you have a GitHub organization, you can create an application under it + in your organization Settings > Developer settings > GitHub Apps > New GitHub App. + + + + 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. + + + 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) + + + +## Set up Dynamic Secrets with GitHub + + + + Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/github/modal.png) + + + + Name by which you want the secret to be referenced + + + The ID of the app created in earlier steps. + + + The Private Key of the app created in earlier steps. + + + The ID of the installation from earlier steps. + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + + + + 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) + + + +## 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) + + + 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. + + +## Renew Leases + + + GitHub App tokens cannot be renewed because they are fixed to a lifetime of 1 hour. + diff --git a/docs/documentation/platform/pki/pki-issuer.mdx b/docs/documentation/platform/pki/pki-issuer.mdx index c46c1f35e..13214bfb7 100644 --- a/docs/documentation/platform/pki/pki-issuer.mdx +++ b/docs/documentation/platform/pki/pki-issuer.mdx @@ -49,11 +49,21 @@ In the following steps, we explore how to install the Infisical PKI Issuer using ``` - 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 - ``` + + + ```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 + ``` + + + ```bash + kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical-issuer/main/build/install.yaml + ``` + + 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. diff --git a/docs/images/app-connections/cloudflare/cloudflare-account-id.png b/docs/images/app-connections/cloudflare/cloudflare-account-id.png new file mode 100644 index 000000000..865b2ff76 Binary files /dev/null and b/docs/images/app-connections/cloudflare/cloudflare-account-id.png differ diff --git a/docs/images/app-connections/cloudflare/cloudflare-app-connection-created.png b/docs/images/app-connections/cloudflare/cloudflare-app-connection-created.png new file mode 100644 index 000000000..168d36da5 Binary files /dev/null and b/docs/images/app-connections/cloudflare/cloudflare-app-connection-created.png differ diff --git a/docs/images/app-connections/cloudflare/cloudflare-app-connection-form.png b/docs/images/app-connections/cloudflare/cloudflare-app-connection-form.png new file mode 100644 index 000000000..78af87a6f Binary files /dev/null and b/docs/images/app-connections/cloudflare/cloudflare-app-connection-form.png differ diff --git a/docs/images/app-connections/cloudflare/cloudflare-app-connection-select.png b/docs/images/app-connections/cloudflare/cloudflare-app-connection-select.png new file mode 100644 index 000000000..85d2c401a Binary files /dev/null and b/docs/images/app-connections/cloudflare/cloudflare-app-connection-select.png differ diff --git a/docs/images/app-connections/cloudflare/cloudflare-create-token.png b/docs/images/app-connections/cloudflare/cloudflare-create-token.png new file mode 100644 index 000000000..7fba350fe Binary files /dev/null and b/docs/images/app-connections/cloudflare/cloudflare-create-token.png differ diff --git a/docs/images/app-connections/cloudflare/cloudflare-generated-token.png b/docs/images/app-connections/cloudflare/cloudflare-generated-token.png new file mode 100644 index 000000000..e470bcbb7 Binary files /dev/null and b/docs/images/app-connections/cloudflare/cloudflare-generated-token.png differ diff --git a/docs/images/app-connections/cloudflare/cloudflare-navigate-profile.png b/docs/images/app-connections/cloudflare/cloudflare-navigate-profile.png new file mode 100644 index 000000000..4285b4c91 Binary files /dev/null and b/docs/images/app-connections/cloudflare/cloudflare-navigate-profile.png differ diff --git a/docs/images/app-connections/cloudflare/cloudflare-pages-configure-permissions.png b/docs/images/app-connections/cloudflare/cloudflare-pages-configure-permissions.png new file mode 100644 index 000000000..d678fca68 Binary files /dev/null and b/docs/images/app-connections/cloudflare/cloudflare-pages-configure-permissions.png differ diff --git a/docs/images/integrations/github/app/self-hosted-github-app-admin-panel.png b/docs/images/integrations/github/app/self-hosted-github-app-admin-panel.png new file mode 100644 index 000000000..2aedd1887 Binary files /dev/null and b/docs/images/integrations/github/app/self-hosted-github-app-admin-panel.png differ diff --git a/docs/images/platform/dynamic-secrets/github/install-app-modal.png b/docs/images/platform/dynamic-secrets/github/install-app-modal.png new file mode 100644 index 000000000..f3aa1c51d Binary files /dev/null and b/docs/images/platform/dynamic-secrets/github/install-app-modal.png differ diff --git a/docs/images/platform/dynamic-secrets/github/install-app.png b/docs/images/platform/dynamic-secrets/github/install-app.png new file mode 100644 index 000000000..7be3d6c1b Binary files /dev/null and b/docs/images/platform/dynamic-secrets/github/install-app.png differ diff --git a/docs/images/platform/dynamic-secrets/github/installation.png b/docs/images/platform/dynamic-secrets/github/installation.png new file mode 100644 index 000000000..61a06ec04 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/github/installation.png differ diff --git a/docs/images/platform/dynamic-secrets/github/lease.png b/docs/images/platform/dynamic-secrets/github/lease.png new file mode 100644 index 000000000..4ce602898 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/github/lease.png differ diff --git a/docs/images/platform/dynamic-secrets/github/modal.png b/docs/images/platform/dynamic-secrets/github/modal.png new file mode 100644 index 000000000..9ac7743a8 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/github/modal.png differ diff --git a/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-created.png b/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-created.png new file mode 100644 index 000000000..f01787acb Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-created.png differ diff --git a/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-destination.png b/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-destination.png new file mode 100644 index 000000000..74d57d66c Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-destination.png differ diff --git a/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-details.png b/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-details.png new file mode 100644 index 000000000..f347600b0 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-details.png differ diff --git a/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-options.png b/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-options.png new file mode 100644 index 000000000..f6e67aabb Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-options.png differ diff --git a/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-review.png b/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-review.png new file mode 100644 index 000000000..a4b43e3cb Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-review.png differ diff --git a/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-source.png b/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-source.png new file mode 100644 index 000000000..e57d2d5be Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-source.png differ diff --git a/docs/images/secret-syncs/cloudflare-pages/select-cloudflare-pages-option.png b/docs/images/secret-syncs/cloudflare-pages/select-cloudflare-pages-option.png new file mode 100644 index 000000000..5246225a5 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-pages/select-cloudflare-pages-option.png differ diff --git a/docs/integrations/app-connections/cloudflare.mdx b/docs/integrations/app-connections/cloudflare.mdx new file mode 100644 index 000000000..66ba1256f --- /dev/null +++ b/docs/integrations/app-connections/cloudflare.mdx @@ -0,0 +1,94 @@ +--- +title: "Cloudflare Connection" +description: "Learn how to configure a Cloudflare Connection for Infisical." +--- + +Infisical supports connecting to Cloudflare using API tokens and Account ID for secure access to your Cloudflare services. + +## Configure API Token and Account ID for Infisical + + + + Navigate to your Cloudflare dashboard and go to **Profile**. + + ![Navigate Cloudflare Profile](/images/app-connections/cloudflare/cloudflare-navigate-profile.png) + + Click **API Tokens > Create Token** to generate a new API token. + + ![Create API Token](/images/app-connections/cloudflare/cloudflare-create-token.png) + + + + Configure your API token with the necessary permissions for your Cloudflare services. + + Depending on your use case, add one or more of the following permission sets to your API token: + + + + + + Use the following permissions to grant Infisical access to sync secrets to Cloudflare Pages: + + ![Configure Token](/images/app-connections/cloudflare/cloudflare-pages-configure-permissions.png) + + **Required Permissions:** + - **Account** - **Cloudflare Pages** - **Edit** + - **Account** - **Account Settings** - **Read** + + Add these permissions to your API token and click **Continue to summary**, then **Create Token** to generate your API token. + + + + + + + + After creation, copy and securely store your API token as it will not be shown again. + + ![Generated API Token](/images/app-connections/cloudflare/cloudflare-generated-token.png) + + + Keep your API token secure and do not share it. Anyone with access to this token can manage your Cloudflare resources based on the permissions granted. + + + + + From your Cloudflare Account Home page, click on the account information dropdown and select **Copy account ID**. + + ![Account ID](/images/app-connections/cloudflare/cloudflare-account-id.png) + + Save your Account ID for use in the next step. + + + + +## Setup Cloudflare Connection in Infisical + + + + Navigate to the **App Connections** tab on the **Organization Settings** + page. ![App Connections + Tab](/images/app-connections/general/add-connection.png) + + + Select the **Cloudflare Connection** option from the connection options + modal. ![Select Cloudflare + Connection](/images/app-connections/cloudflare/cloudflare-app-connection-select.png) + + + Enter your Cloudflare API token and Account ID in the provided fields and + click **Connect to Cloudflare** to establish the connection. ![Connect to + Cloudflare](/images/app-connections/cloudflare/cloudflare-app-connection-form.png) + + + Your **Cloudflare Connection** is now available for use in your Infisical + projects. ![Cloudflare Connection + Created](/images/app-connections/cloudflare/cloudflare-app-connection-created.png) + + + + + API token connections require manual token rotation when your Cloudflare API + token expires or is regenerated. Monitor your connection status and update the + token as needed. + diff --git a/docs/integrations/app-connections/github.mdx b/docs/integrations/app-connections/github.mdx index 2f8caffed..8f4283ae9 100644 --- a/docs/integrations/app-connections/github.mdx +++ b/docs/integrations/app-connections/github.mdx @@ -53,7 +53,22 @@ Infisical supports two methods for connecting to GitHub. Obtain the necessary Github application credentials. This would be the application slug, client ID, app ID, client secret, and private key. ![integrations github app credentials](/images/integrations/github/app/self-hosted-github-app-credentials.png) - Back in your Infisical instance, add the five new environment variables for the credentials of your GitHub application: + Back in your Infisical instance, you can configure the GitHub App credentials in one of two ways: + + **Option 1: Server Admin Panel (Recommended)** + + Navigate to the server admin panel > **Integrations** > **GitHub App** and enter the GitHub application credentials: + ![integrations github app admin panel](/images/integrations/github/app/self-hosted-github-app-admin-panel.png) + + - **Client ID**: The Client ID of your GitHub application + - **Client Secret**: The Client Secret of your GitHub application + - **App Slug**: The Slug of your GitHub application (found in the URL) + - **App ID**: The App ID of your GitHub application + - **Private Key**: The Private Key of your GitHub application + + **Option 2: Environment Variables** + + Alternatively, you can add the new environment variables for the credentials of your GitHub application: - `INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID`: The **Client ID** of your GitHub application. - `INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET`: The **Client Secret** of your GitHub application. @@ -61,7 +76,7 @@ Infisical supports two methods for connecting to GitHub. - `INF_APP_CONNECTION_GITHUB_APP_ID`: The **App ID** of your GitHub application. - `INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY`: The **Private Key** of your GitHub application. - Once added, restart your Infisical instance and use the GitHub integration via app authentication. + Once configured, you can use the GitHub integration via app authentication. If you configured the credentials using environment variables, restart your Infisical instance for the changes to take effect. If you configured them through the server admin panel, allow approximately 5 minutes for the changes to propagate. @@ -158,4 +173,5 @@ Infisical supports two methods for connecting to GitHub. + diff --git a/docs/integrations/secret-syncs/cloudflare-pages.mdx b/docs/integrations/secret-syncs/cloudflare-pages.mdx new file mode 100644 index 000000000..1375de7a7 --- /dev/null +++ b/docs/integrations/secret-syncs/cloudflare-pages.mdx @@ -0,0 +1,133 @@ +--- +title: "Cloudflare Pages Sync" +description: "Learn how to configure a Cloudflare Pages Sync for Infisical." +--- + +**Prerequisites:** + +- Set up and add secrets to [Infisical Cloud](https://app.infisical.com) +- Create a [Cloudflare Connection](/integrations/app-connections/cloudflare) + + + + 1. Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + 2. Select the **Cloudflare Pages** option. + ![Select Cloudflare Pages](/images/secret-syncs/cloudflare-pages/select-cloudflare-pages-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + 4. Configure the **Destination** to where secrets should be deployed, then click **Next**. + ![Configure Destination](/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-destination.png) + + - **Cloudflare Connection**: The Cloudflare Connection to authenticate with. + - **Cloudflare Pages Project**: Choose the Cloudflare Pages project you want to sync secrets to. + - **Environment**: Select the deployment environment (preview or production). + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + 6. Configure the **Details** of your Cloudflare Pages Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Cloudflare Pages Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-review.png) + + 8. If enabled, your Cloudflare Pages Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/cloudflare-pages/cloudflare-pages-sync-created.png) + + + + To create a **Cloudflare Pages Sync**, make an API request to the [Create Cloudflare Pages Sync](/api-reference/endpoints/secret-syncs/cloudflare-pages/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/cloudflare-pages \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-cloudflare-pages-sync", + "projectId": "your-project-id", + "description": "an example sync", + "connectionId": "your-cloudflare-connection-id", + "environment": "production", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "projectId": "your-cloudflare-pages-project-id", + "projectName": "my-pages-project", + "environment": "production" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "your-sync-id", + "name": "my-cloudflare-pages-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "your-folder-id", + "connectionId": "your-cloudflare-connection-id", + "createdAt": "2024-05-01T12:00:00Z", + "updatedAt": "2024-05-01T12:00:00Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2024-05-01T12:00:00Z", + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "your-project-id", + "connection": { + "app": "cloudflare", + "name": "my-cloudflare-connection", + "id": "your-cloudflare-connection-id" + }, + "environment": { + "slug": "production", + "name": "Production", + "id": "your-env-id" + }, + "folder": { + "id": "your-folder-id", + "path": "/my-secrets" + }, + "destination": "cloudflare-pages", + "destinationConfig": { + "projectId": "your-cloudflare-pages-project-id", + "projectName": "my-pages-project", + "environment": "production" + } + } + } + ``` + + + diff --git a/docs/internals/architecture/cloud.mdx b/docs/internals/architecture/cloud.mdx new file mode 100644 index 000000000..381e93d33 --- /dev/null +++ b/docs/internals/architecture/cloud.mdx @@ -0,0 +1,128 @@ +--- +title: "Infisical Cloud" +description: "Architecture overview of Infisical's US and EU cloud deployments" +--- + +This document provides an overview of Infisical's cloud architecture for our US and EU deployments, detailing the core components and how they interact to provide security and infrastructure services. + +## Overview + +Infisical Cloud operates on AWS infrastructure using containerized services deployed via Amazon ECS (Elastic Container Service). Our US and EU deployments use identical architectural patterns to ensure consistency and reliability across regions. + +![Infisical Cloud Architecture](/images/self-hosting/reference-architectures/Infisical-AWS-ECS-architecture.jpeg) + +## Components + +A typical Infisical Cloud deployment consists of the following components: + +### Application Services + +- **Infisical Core**: Main application server running the Infisical backend API +- **License API**: Dedicated API service for license management with separate RDS instance (shared between US/EU) +- **Application Load Balancer**: Routes incoming traffic to application containers with SSL termination and host-based routing + +### Data Layer + +- **Amazon RDS (PostgreSQL)**: + - **Main RDS Instance**: Primary database for secrets, users, and metadata (Multi-AZ, encryption enabled) + - **License API RDS Instance**: Dedicated database for license management services +- **Amazon ElastiCache (Redis)**: + - **Main Redis Cluster**: Multi-AZ replication group for core application caching and queuing + - **License API Redis**: Dedicated cache for license services + - Redis 7 engine with CloudWatch logging and snapshot backups + +### Infrastructure + +- **ECS Fargate**: Serverless container platform running application services +- **AWS Global Accelerator**: Global traffic routing and performance optimization +- **Cloudflare**: DNS management and routing +- **AWS SSM Parameter Store**: Stores application configuration and secrets +- **CloudWatch**: Centralized logging and monitoring + +## System Layout + +### Service Architecture + +The Infisical application runs as multiple containerized services on ECS: + +- **Main Server**: Auto-scaling containerized application services +- **License API**: Dedicated service with separate infrastructure (shared globally) +- **Monitoring**: AWS OTel Collector and Datadog Agent sidecars + +Container images are pulled from Docker Hub and managed via GitHub Actions for deployments. + +### Network Configuration + +Services are deployed in private subnets with the following connectivity: + +- External traffic → Application Load Balancer → ECS Services +- Main server exposes port 8080 +- License API exposes port 4000 (portal.infisical.com, license.infisical.com) +- Service-to-service communication via AWS Service Connect + +### Data Flow + +1. **DNS resolution** via Cloudflare routes traffic to AWS Global Accelerator +2. **Global Accelerator** optimizes routing to the nearest AWS region +3. **Client requests** are routed through the Application Load Balancer to ECS containers +4. **Application logic** processes requests in the Infisical Core service +5. **Data persistence** occurs via encrypted connections to RDS +6. **Caching** utilizes ElastiCache for performance optimization +7. **Configuration** is retrieved from AWS SSM Parameter Store + +## Regional Deployments + +Each region operates in a separate AWS account, providing strong isolation boundaries for security, compliance, and operational independence. + +### US Cloud (us.infisical.com or app.infisical.com) + +- **AWS Account**: Dedicated US AWS account +- **Infrastructure**: ECS-based containerized deployment +- **Monitoring**: Integrated with Datadog for observability and security monitoring + +### EU Cloud (eu.infisical.com) + +- **AWS Account**: Dedicated EU AWS account +- **Infrastructure**: ECS-based containerized deployment +- **Monitoring**: Integrated with Datadog for observability and security monitoring + +## Configuration Management + +Application configuration and secrets are managed through AWS SSM Parameter Store, with deployment automation handled via GitHub Actions. + +## Monitoring and Observability + +### Logging + +- **CloudWatch**: 365-day retention for application logs +- **Health Checks**: HTTP endpoint monitoring for service health + +### Metrics + +- **AWS OTel Collector**: Prometheus metrics collection +- **Datadog Agent**: Application performance monitoring and infrastructure metrics + +## Container Management + +- **Images**: `infisical/staging_infisical` and `infisical/license-api` from Docker Hub +- **Deployment**: Automated via GitHub Actions updating SSM parameter for image tags +- **Registry Access**: Docker Hub credentials stored in AWS Secrets Manager +- **Platform**: ECS Fargate serverless container platform + +## Security Overview + +### Data Protection + +- **Encryption**: All secrets encrypted at rest and in transit +- **Network Isolation**: Services deployed in private subnets with controlled access +- **Authentication**: API tokens and service accounts for secure access +- **Audit Logging**: Comprehensive audit trails for all secret operations + +### Network Architecture + +- **VPC Design**: Dedicated VPC with public and private subnets across multiple Availability Zones +- **NAT Gateway**: Controlled outbound connectivity from private subnets +- **Load Balancing**: Application Load Balancer with SSL termination and health checks +- **Security Groups**: Restrictive firewall rules and controlled network access +- **High Availability**: Multi-AZ deployment with automatic failover +- **Network Monitoring**: VPC Flow Logs with 365-day retention for traffic analysis diff --git a/docs/internals/components.mdx b/docs/internals/architecture/components.mdx similarity index 100% rename from docs/internals/components.mdx rename to docs/internals/architecture/components.mdx diff --git a/docs/self-hosting/guides/automated-bootstrapping.mdx b/docs/self-hosting/guides/automated-bootstrapping.mdx index ebc9c3c80..3c2186eb9 100644 --- a/docs/self-hosting/guides/automated-bootstrapping.mdx +++ b/docs/self-hosting/guides/automated-bootstrapping.mdx @@ -5,13 +5,13 @@ description: "Learn how to provision and configure Infisical instances programma Infisical's Automated Bootstrapping feature enables you to provision and configure an Infisical instance without using the UI, allowing for complete automation through static configuration files, API calls, or CLI commands. This is especially valuable for enterprise environments where automated deployment and infrastructure-as-code practices are essential. -## Overview +The bootstrapping workflow automates creating an admin user account, initializing an organization for the entire instance, establishing an **instance admin machine identity** with full administrative permissions, and returning the machine identity credentials for further automation. -The Automated Bootstrapping workflow automates the following processes: -- Creating an admin user account -- Initializing an organization for the entire instance -- Establishing an **instance admin machine identity** with full administrative permissions -- Returning the machine identity credentials for further automation +## Prerequisites + +- An Infisical instance launched with all required configuration variables +- Access to the Infisical CLI or the ability to make API calls to the instance +- Network connectivity to the Infisical instance ## Key Concepts @@ -20,15 +20,9 @@ The Automated Bootstrapping workflow automates the following processes: ![Instance Admin Identity](/images/self-hosting/guides/automated-bootstrapping/identity-instance-admin.png) - **Token Auth**: The instance admin machine identity uses [Token Auth](/documentation/platform/identities/token-auth), providing a JWT token that can be used directly to make authenticated requests to the Infisical API. -## Prerequisites - -- An Infisical instance launched with all required configuration variables -- Access to the Infisical CLI or the ability to make API calls to the instance -- Network connectivity to the Infisical instance - ## Bootstrap Methods -You can bootstrap an Infisical instance using either the API or the CLI. +You can bootstrap an Infisical instance using the API, CLI, or Helm chart. @@ -51,6 +45,37 @@ You can bootstrap an Infisical instance using either the API or the CLI. -d '{"email":"admin@example.com","password":"your-secure-password","organization":"your-org-name"}' \ http://your-infisical-instance.com/api/v1/admin/bootstrap ``` + + ### API Response Structure + + The bootstrap process returns a JSON response with details about the created user, organization, and machine identity: + + ```json + { + "identity": { + "credentials": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y" + }, + "id": "db224792-ed18-4277-9dae-57e752315854", + "name": "Instance Admin Identity" + }, + "message": "Successfully bootstrapped instance", + "organization": { + "id": "b56bece0-42f5-4262-b25e-be7bf5f84957", + "name": "dog", + "slug": "dog-v-e5l" + }, + "user": { + "email": "admin@example.com", + "firstName": "Admin", + "id": "a418f355-c8da-453c-bbc8-6c07208eeb3c", + "lastName": "User", + "superAdmin": true, + "username": "admin@example.com" + } + } + ``` + Use the [Infisical CLI](/cli/commands/bootstrap) to bootstrap the instance and extract the token for immediate use in automation: @@ -60,39 +85,126 @@ You can bootstrap an Infisical instance using either the API or the CLI. ``` This example command pipes the output through `jq` to extract only the machine identity token, making it easy to capture and use directly in automation scripts or export as an environment variable for tools like Terraform. + + ### API Response Structure + + The bootstrap process returns a JSON response with details about the created user, organization, and machine identity: + + ```json + { + "identity": { + "credentials": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y" + }, + "id": "db224792-ed18-4277-9dae-57e752315854", + "name": "Instance Admin Identity" + }, + "message": "Successfully bootstrapped instance", + "organization": { + "id": "b56bece0-42f5-4262-b25e-be7bf5f84957", + "name": "dog", + "slug": "dog-v-e5l" + }, + "user": { + "email": "admin@example.com", + "firstName": "Admin", + "id": "a418f355-c8da-453c-bbc8-6c07208eeb3c", + "lastName": "User", + "superAdmin": true, + "username": "admin@example.com" + } + } + ``` + + + When deploying Infisical using the official [Helm chart](/self-hosting/deployment-options/kubernetes-helm#kubernetes-via-helm-chart), you can enable automatic bootstrapping that runs as part of the deployment process. This eliminates the need to manually bootstrap the instance after deployment. + + The bootstrapping process automatically generates a Kubernetes secret containing the instance admin token, which can then be referenced by Crossplane providers, Terraform operators, or other automation systems for further infrastructure provisioning and configuration. + + ### Configuration + + Enable auto bootstrapping in your Helm values by setting `autoBootstrap.enabled: true` and providing the necessary configuration: + + ```yaml + autoBootstrap: + enabled: true + organization: "My Organization" + secretTemplate: '{"data":{"token":"{{.Identity.Credentials.Token}}"}}' + + secretDestination: + name: "infisical-bootstrap-secret" + namespace: "default" # defaults to release namespace if not specified + + credentialSecret: + name: "infisical-bootstrap-credentials" + ``` + + You'll also need to create a secret containing the bootstrap credentials before deployment. The secret must contain `INFISICAL_ADMIN_EMAIL` and `INFISICAL_ADMIN_PASSWORD` keys: + + ```bash + kubectl create secret generic infisical-bootstrap-credentials \ + --from-literal=INFISICAL_ADMIN_EMAIL="admin@example.com" \ + --from-literal=INFISICAL_ADMIN_PASSWORD="your-secure-password" \ + --namespace=release-namespace + ``` + + ### How It Works + + The Helm chart auto bootstrap feature: + + 1. **Post-Install Hook**: Runs automatically after the main Infisical deployment is complete + 2. **Readiness Check**: Uses an init container with curl to wait for Infisical to be ready by polling the `/api/status` endpoint + 3. **Bootstrap Execution**: Uses the Infisical CLI to bootstrap the instance + 4. **Kubernetes Secret Creation**: Creates a Kubernetes secret directly via the Kubernetes API using the rendered template + 5. **RBAC**: Automatically configures the necessary permissions (`get`, `create`, `update` on secrets) for the bootstrap job + + ### Template System + + The `secretTemplate` field allows you to customize the data section of the created Kubernetes secret. The template has access to the full bootstrap response with the following available data fields: + + - `{{ .Identity.Credentials.Token }}`: The admin machine identity token + - `{{ .Identity.ID }}`: The identity ID + - `{{ .Identity.Name }}`: The identity name + - `{{ .Organization.ID }}`: The organization ID + - `{{ .Organization.Name }}`: The organization name + - `{{ .Organization.Slug }}`: The organization slug + - `{{ .User.Email }}`: The admin user email + - `{{ .User.ID }}`: The admin user ID + - `{{ .User.FirstName }}`: The admin user first name + - `{{ .User.LastName }}`: The admin user last name + + The template also supports the `encodeBase64` function for base64 encoding values. + + Example template for storing multiple values: + + ```yaml + secretTemplate: | + { + "data": { + "infisical_token": "{{ .Identity.Credentials.Token }}", + "admin_email": "{{ .User.Email }}", + "organization": "{{ .Organization.Name }}" + } + } + ``` + + ### Benefits + + - **Zero-Touch Deployment**: Complete Infisical setup without manual intervention + - **Infrastructure as Code**: Bootstrap configuration is versioned with your Helm values + - **Secure Token Storage**: Admin identity credentials are immediately stored in Kubernetes secrets + - **Integration Ready**: The created secret can be referenced by other applications or automation tools + + ### Security Considerations + + - The bootstrap job requires permissions to create secrets in the specified namespace + - Bootstrap credentials should be stored securely and rotated regularly + - The generated admin token has full instance privileges and should be protected accordingly + - Consider using Kubernetes RBAC to restrict access to the generated secret + -## API Response Structure - -The bootstrap process returns a JSON response with details about the created user, organization, and machine identity: - -```json -{ - "identity": { - "credentials": { - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eUlkIjoiZGIyMjQ3OTItZWQxOC00Mjc3LTlkYWUtNTdlNzUyMzE1ODU0IiwiaWRlbnRpdHlBY2Nlc3NUb2tlbklkIjoiZmVkZmZmMGEtYmU3Yy00NjViLWEwZWEtZjM5OTNjMTg4OGRlIiwiYXV0aFRva2VuVHlwZSI6ImlkZW50aXR5QWNjZXNzVG9rZW4iLCJpYXQiOjE3NDIzMjI0ODl9.mqcZZqIFqER1e9ubrQXp8FbzGYi8nqqZwfMvz09g-8Y" - }, - "id": "db224792-ed18-4277-9dae-57e752315854", - "name": "Instance Admin Identity" - }, - "message": "Successfully bootstrapped instance", - "organization": { - "id": "b56bece0-42f5-4262-b25e-be7bf5f84957", - "name": "dog", - "slug": "dog-v-e5l" - }, - "user": { - "email": "admin@example.com", - "firstName": "Admin", - "id": "a418f355-c8da-453c-bbc8-6c07208eeb3c", - "lastName": "User", - "superAdmin": true, - "username": "admin@example.com" - } -} -``` - ## Using the Instance Admin Machine Identity Token The bootstrap process automatically creates a machine identity with Token Auth configured. The returned token has instance-level admin privileges (the highest level of access) and should be treated with the same security considerations as a root credential. diff --git a/docs/style.css b/docs/style.css index 4d9877c6c..ee0b6c66b 100644 --- a/docs/style.css +++ b/docs/style.css @@ -1,3 +1,7 @@ +* { + border-radius: 0 !important; +} + #navbar .max-w-8xl { max-width: 100%; border-bottom: 1px solid #ebebeb; @@ -26,24 +30,20 @@ } #sidebar li > div.mt-2 { - border-radius: 0; padding: 5px; } #sidebar li > a.text-primary { - border-radius: 0; background-color: #FBFFCC; border-left: 4px solid #EFFF33; padding: 5px; } #sidebar li > a.mt-2 { - border-radius: 0; padding: 5px; } #sidebar li > a.leading-6 { - border-radius: 0; padding: 0px; } @@ -68,65 +68,26 @@ } #content-area .mt-8 .block{ - border-radius: 0; border-width: 1px; background-color: #FCFBFA; border-color: #ebebeb; } /* #content-area:hover .mt-8 .block:hover{ - border-radius: 0; + border-width: 1px; background-color: #FDFFE5; border-color: #EFFF33; } */ -#content-area .mt-8 .rounded-xl{ - border-radius: 0; -} - -#content-area .mt-8 .rounded-lg{ - border-radius: 0; -} - -#content-area .mt-6 .rounded-xl{ - border-radius: 0; -} - -#content-area .mt-6 .rounded-lg{ - border-radius: 0; -} - -#content-area .mt-6 .rounded-md{ - border-radius: 0; -} - -#content-area .mt-8 .rounded-md{ - border-radius: 0; -} - #content-area div.my-4{ - border-radius: 0; border-width: 1px; } -#content-area div.flex-1 { - /* text-transform: uppercase; */ +/* #content-area div.flex-1 { opacity: 0.8; font-weight: 400; -} - -#content-area button { - border-radius: 0; -} - -#content-area a { - border-radius: 0; -} - -#content-area .not-prose { - border-radius: 0; -} +} */ /* .eyebrow { text-transform: uppercase; diff --git a/frontend/src/components/features/TtlFormLabel.tsx b/frontend/src/components/features/TtlFormLabel.tsx index 02f162200..fdb9aea5e 100644 --- a/frontend/src/components/features/TtlFormLabel.tsx +++ b/frontend/src/components/features/TtlFormLabel.tsx @@ -26,7 +26,7 @@ export const TtlFormLabel = ({ label }: { label: string }) => ( } diff --git a/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx b/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx index a7c211bff..68866dcbd 100644 --- a/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx +++ b/frontend/src/components/secret-syncs/CreateSecretSyncModal.tsx @@ -59,7 +59,6 @@ export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...prop onPointerDownOutside={(e) => e.preventDefault()} className="max-w-2xl" subTitle={selectedSync ? undefined : "Select a third-party service to sync secrets to."} - bodyClassName="overflow-visible" > { diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx new file mode 100644 index 000000000..d19092d63 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx @@ -0,0 +1,95 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2"; +import { + TCloudflareProject, + useCloudflareConnectionListPagesProjects +} from "@app/hooks/api/appConnections/cloudflare"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +const CLOUDFLARE_ENVIRONMENTS = [ + { + name: "Preview", + value: "preview" + }, + { + name: "Production", + value: "production" + } +]; + +export const CloudflarePagesSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.CloudflarePages } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: projects = [], isPending: isProjectsPending } = + useCloudflareConnectionListPagesProjects(connectionId, { + enabled: Boolean(connectionId) + }); + + return ( + <> + { + setValue("destinationConfig.projectName", ""); + setValue("destinationConfig.environment", "preview"); + }} + /> + ( + + project.name === value) ?? []) : []} + onChange={(option) => { + onChange((option as SingleValue)?.name ?? null); + }} + options={projects} + placeholder="Select a project..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id.toString()} + /> + + )} + /> + ( + + + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 8ea670982..b6074d270 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -10,6 +10,7 @@ import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFiel import { AzureDevOpsSyncFields } from "./AzureDevOpsSyncFields"; import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields"; import { CamundaSyncFields } from "./CamundaSyncFields"; +import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields"; import { FlyioSyncFields } from "./FlyioSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; @@ -73,6 +74,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.GitLab: return ; + case SecretSync.CloudflarePages: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 5958b49c0..a61c2a6b8 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -56,6 +56,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Render: case SecretSync.Flyio: case SecretSync.GitLab: + case SecretSync.CloudflarePages: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/CloudflarePagesReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/CloudflarePagesReviewFields.tsx new file mode 100644 index 000000000..ab18be5da --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/CloudflarePagesReviewFields.tsx @@ -0,0 +1,18 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const CloudflarePagesSyncReviewFields = () => { + const { watch } = useFormContext(); + const projectName = watch("destinationConfig.projectName"); + const environment = watch("destinationConfig.environment"); + + return ( + <> + {projectName} + {environment} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 82aedabc6..fb639e91b 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -19,6 +19,7 @@ import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSy import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields"; import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; +import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; import { FlyioSyncReviewFields } from "./FlyioSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; @@ -120,6 +121,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.GitLab: DestinationFieldsComponent = ; break; + case SecretSync.CloudflarePages: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/cloudflare-pages-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/cloudflare-pages-sync-destination-schema.ts new file mode 100644 index 000000000..c44a579db --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/cloudflare-pages-sync-destination-schema.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const CloudflarePagesSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.CloudflarePages), + destinationConfig: z.object({ + projectName: z.string().trim().min(1, "Project name is required"), + environment: z.string().trim().min(1, "Environment is required") + }) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 15e7b0f03..5d15492eb 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -7,6 +7,7 @@ import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configur import { AzureDevOpsSyncDestinationSchema } from "./azure-devops-sync-destination-schema"; import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema"; import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"; +import { CloudflarePagesSyncDestinationSchema } from "./cloudflare-pages-sync-destination-schema"; import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema"; import { FlyioSyncDestinationSchema } from "./flyio-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; @@ -43,7 +44,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ HerokuSyncDestinationSchema, RenderSyncDestinationSchema, FlyioSyncDestinationSchema, - GitlabSyncDestinationSchema + GitlabSyncDestinationSchema, + CloudflarePagesSyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/components/utilities/attemptCliLogin.ts b/frontend/src/components/utilities/attemptCliLogin.ts index 914a4c266..d897ebeaa 100644 --- a/frontend/src/components/utilities/attemptCliLogin.ts +++ b/frontend/src/components/utilities/attemptCliLogin.ts @@ -16,7 +16,7 @@ export interface IsCliLoginSuccessful { loginResponse?: { email: string; privateKey: string; - JWTToken: string; + JTWToken: string; }; success: boolean; } @@ -131,7 +131,7 @@ const attemptLogin = async ({ loginResponse: { email, privateKey, - JWTToken: token + JTWToken: token }, success: true }); diff --git a/frontend/src/components/utilities/attemptCliLoginMfa.ts b/frontend/src/components/utilities/attemptCliLoginMfa.ts index 2a5de18dd..14a61d817 100644 --- a/frontend/src/components/utilities/attemptCliLoginMfa.ts +++ b/frontend/src/components/utilities/attemptCliLoginMfa.ts @@ -14,7 +14,7 @@ interface IsMfaLoginSuccessful { success: boolean; loginResponse: { privateKey: string; - JWTToken: string; + JTWToken: string; }; } @@ -95,7 +95,7 @@ const attemptLoginMfa = async ({ success: true, loginResponse: { privateKey, - JWTToken: token + JTWToken: token } }); } catch (err) { diff --git a/frontend/src/components/v2/Dropdown/Dropdown.tsx b/frontend/src/components/v2/Dropdown/Dropdown.tsx index c4cc95429..b1831187a 100644 --- a/frontend/src/components/v2/Dropdown/Dropdown.tsx +++ b/frontend/src/components/v2/Dropdown/Dropdown.tsx @@ -94,7 +94,7 @@ export const DropdownMenuItem = ({ className={twMerge( "block cursor-pointer rounded-sm px-4 py-2 font-inter text-xs text-mineshaft-200 outline-none data-[highlighted]:bg-mineshaft-700", className, - isDisabled ? "pointer-events-none opacity-50" : "" + isDisabled ? "pointer-events-none cursor-not-allowed opacity-50" : "" )} > diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 871985ce1..a8f0fb385 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -16,6 +16,12 @@ export const ROUTE_PATHS = Object.freeze({ PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset"), PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup") }, + Admin: { + IntegrationsPage: setRoute( + "/admin/integrations", + "/_authenticate/_inject-org-details/admin/_admin-layout/integrations" + ) + }, Organization: { Settings: { OauthCallbackPage: setRoute( diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 048826357..612ceeea8 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -18,6 +18,7 @@ import { AzureDevOpsConnectionMethod, AzureKeyVaultConnectionMethod, CamundaConnectionMethod, + CloudflareConnectionMethod, DatabricksConnectionMethod, FlyioConnectionMethod, GcpConnectionMethod, @@ -86,7 +87,8 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Heroku]: { name: "Heroku", image: "Heroku.png" }, [AppConnection.Render]: { name: "Render", image: "Render.png" }, [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" }, - [AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" } + [AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" }, + [AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -117,6 +119,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case TerraformCloudConnectionMethod.ApiToken: case VercelConnectionMethod.ApiToken: case OnePassConnectionMethod.ApiToken: + case CloudflareConnectionMethod.ApiToken: return { name: "API Token", icon: faKey }; case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: diff --git a/frontend/src/helpers/policies.ts b/frontend/src/helpers/policies.ts index 7828807dc..d798d3c10 100644 --- a/frontend/src/helpers/policies.ts +++ b/frontend/src/helpers/policies.ts @@ -1,12 +1,20 @@ +import { IconDefinition } from "@fortawesome/free-brands-svg-icons"; +import { faArrowRightToBracket, faEdit } from "@fortawesome/free-solid-svg-icons"; + import { PolicyType } from "@app/hooks/api/policies/enums"; -export const policyDetails: Record = { +export const policyDetails: Record< + PolicyType, + { name: string; className: string; icon: IconDefinition } +> = { [PolicyType.AccessPolicy]: { - className: "bg-lime-900 text-lime-100", - name: "Access Policy" + className: "bg-green/20 text-green", + name: "Access Policy", + icon: faArrowRightToBracket }, [PolicyType.ChangePolicy]: { - className: "bg-indigo-900 text-indigo-100", - name: "Change Policy" + className: "bg-yellow/20 text-yellow", + name: "Change Policy", + icon: faEdit } }; diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index a4eb513f5..d6395397d 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -77,6 +77,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.Heroku]: AppConnection.Heroku, [SecretSync.Render]: AppConnection.Render, [SecretSync.Flyio]: AppConnection.Flyio, - [SecretSync.GitLab]: AppConnection.Gitlab + [SecretSync.GitLab]: AppConnection.Gitlab, + [SecretSync.CloudflarePages]: AppConnection.Cloudflare }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/accessApproval/queries.tsx b/frontend/src/hooks/api/accessApproval/queries.tsx index 6370f4a59..f5478cd1b 100644 --- a/frontend/src/hooks/api/accessApproval/queries.tsx +++ b/frontend/src/hooks/api/accessApproval/queries.tsx @@ -65,11 +65,11 @@ const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequests const fetchApprovalRequests = async ({ projectSlug, envSlug, - authorProjectMembershipId + authorUserId }: TGetAccessApprovalRequestsDTO) => { const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>( "/api/v1/access-approvals/requests", - { params: { projectSlug, envSlug, authorProjectMembershipId } } + { params: { projectSlug, envSlug, authorUserId } } ); return data.requests.map((request) => ({ @@ -109,12 +109,12 @@ export const useGetAccessRequestsCount = ({ export const useGetAccessApprovalPolicies = ({ projectSlug, envSlug, - authorProjectMembershipId, + authorUserId, options = {} }: TGetAccessApprovalRequestsDTO & TReactQueryOptions) => useQuery({ queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug), - queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorProjectMembershipId }), + queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorUserId }), ...options, enabled: Boolean(projectSlug) && (options?.enabled ?? true) }); @@ -122,16 +122,13 @@ export const useGetAccessApprovalPolicies = ({ export const useGetAccessApprovalRequests = ({ projectSlug, envSlug, - authorProjectMembershipId, + authorUserId, options = {} }: TGetAccessApprovalRequestsDTO & TReactQueryOptions) => useQuery({ - queryKey: accessApprovalKeys.getAccessApprovalRequests( - projectSlug, - envSlug, - authorProjectMembershipId - ), - queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorProjectMembershipId }), + queryKey: accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, authorUserId), + queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorUserId }), ...options, - enabled: Boolean(projectSlug) && (options?.enabled ?? true) + enabled: Boolean(projectSlug) && (options?.enabled ?? true), + placeholderData: (previousData) => previousData }); diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index 40725a16c..32baa3c62 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -35,7 +35,7 @@ export type Approver = { id: string; type: ApproverType; sequence?: number; - approvals?: number; + approvalsRequired?: number; }; export type Bypasser = { @@ -148,7 +148,7 @@ export type TCreateAccessRequestDTO = { export type TGetAccessApprovalRequestsDTO = { projectSlug: string; envSlug?: string; - authorProjectMembershipId?: string; + authorUserId?: string; }; export type TGetAccessPolicyApprovalCountDTO = { diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 149014d85..c5d92b9da 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -56,6 +56,11 @@ export type TUpdateServerConfigDTO = { microsoftTeamsAppId?: string; microsoftTeamsClientSecret?: string; microsoftTeamsBotId?: string; + gitHubAppConnectionClientId?: string; + gitHubAppConnectionClientSecret?: string; + gitHubAppConnectionSlug?: string; + gitHubAppConnectionId?: string; + gitHubAppConnectionPrivateKey?: string; } & Partial; export type TCreateAdminUserDTO = { @@ -100,6 +105,13 @@ export type AdminIntegrationsConfig = { clientSecret: string; botId: string; }; + gitHubAppConnection: { + clientId: string; + clientSecret: string; + appSlug: string; + appId: string; + privateKey: string; + }; }; export type TGetServerRootKmsEncryptionDetails = { diff --git a/frontend/src/hooks/api/appConnections/cloudflare/index.ts b/frontend/src/hooks/api/appConnections/cloudflare/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/cloudflare/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/cloudflare/queries.tsx b/frontend/src/hooks/api/appConnections/cloudflare/queries.tsx new file mode 100644 index 000000000..ae2efea99 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/cloudflare/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; +import { TCloudflareProject } from "./types"; + +const cloudflareConnectionKeys = { + all: [...appConnectionKeys.all, "cloudflare"] as const, + listPagesProjects: (connectionId: string) => + [...cloudflareConnectionKeys.all, "pages-projects", connectionId] as const +}; + +export const useCloudflareConnectionListPagesProjects = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TCloudflareProject[], + unknown, + TCloudflareProject[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: cloudflareConnectionKeys.listPagesProjects(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/cloudflare/${connectionId}/cloudflare-pages-projects` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/cloudflare/types.ts b/frontend/src/hooks/api/appConnections/cloudflare/types.ts new file mode 100644 index 000000000..ed7bda478 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/cloudflare/types.ts @@ -0,0 +1,4 @@ +export type TCloudflareProject = { + id: string; + name: string; +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index c316da5d7..8097720a7 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -26,5 +26,6 @@ export enum AppConnection { Heroku = "heroku", Render = "render", Flyio = "flyio", - Gitlab = "gitlab" + Gitlab = "gitlab", + Cloudflare = "cloudflare" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index d0664b36c..b71370da7 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -128,6 +128,10 @@ export type TGitlabConnectionOption = TAppConnectionOptionBase & { oauthClientId?: string; }; +export type TCloudflareConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Cloudflare; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -154,7 +158,8 @@ export type TAppConnectionOption = | THerokuConnectionOption | TRenderConnectionOption | TFlyioConnectionOption - | TGitlabConnectionOption; + | TGitlabConnectionOption + | TCloudflareConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -185,4 +190,5 @@ export type TAppConnectionOptionMap = { [AppConnection.Render]: TRenderConnectionOption; [AppConnection.Flyio]: TFlyioConnectionOption; [AppConnection.Gitlab]: TGitlabConnectionOption; + [AppConnection.Cloudflare]: TCloudflareConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/cloudflare-connection.ts b/frontend/src/hooks/api/appConnections/types/cloudflare-connection.ts new file mode 100644 index 000000000..6ef7dda95 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/cloudflare-connection.ts @@ -0,0 +1,14 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum CloudflareConnectionMethod { + ApiToken = "api-token" +} + +export type TCloudflareConnection = TRootAppConnection & { app: AppConnection.Cloudflare } & { + method: CloudflareConnectionMethod.ApiToken; + credentials: { + apiToken: string; + accountId: string; + }; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 61ddacbf4..4352cf7fc 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -8,6 +8,7 @@ import { TAzureClientSecretsConnection } from "./azure-client-secrets-connection import { TAzureDevOpsConnection } from "./azure-devops-connection"; import { TAzureKeyVaultConnection } from "./azure-key-vault-connection"; import { TCamundaConnection } from "./camunda-connection"; +import { TCloudflareConnection } from "./cloudflare-connection"; import { TDatabricksConnection } from "./databricks-connection"; import { TFlyioConnection } from "./flyio-connection"; import { TGcpConnection } from "./gcp-connection"; @@ -37,6 +38,7 @@ export * from "./azure-client-secrets-connection"; export * from "./azure-devops-connection"; export * from "./azure-key-vault-connection"; export * from "./camunda-connection"; +export * from "./cloudflare-connection"; export * from "./databricks-connection"; export * from "./flyio-connection"; export * from "./gcp-connection"; @@ -86,7 +88,8 @@ export type TAppConnection = | THerokuConnection | TRenderConnection | TFlyioConnection - | TGitlabConnection; + | TGitlabConnection + | TCloudflareConnection; export type TAvailableAppConnection = Pick; @@ -142,4 +145,5 @@ export type TAppConnectionMap = { [AppConnection.Render]: TRenderConnection; [AppConnection.Flyio]: TFlyioConnection; [AppConnection.Gitlab]: TGitlabConnection; + [AppConnection.Cloudflare]: TCloudflareConnection; }; diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 9acd2308f..796fd3152 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -73,7 +73,6 @@ export const selectOrganization = async (data: { }) => { const { data: res } = await apiRequest.post<{ token: string; - refreshToken: string; isMfaEnabled: boolean; mfaMethod?: MfaMethod; }>("/api/v3/auth/select-organization", data); diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index e8d80e632..4dc635fa2 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -12,9 +12,10 @@ export type TDynamicSecret = { defaultTTL: string; status?: DynamicSecretStatus; statusDetails?: string; - maxTTL: string; + maxTTL?: string; usernameTemplate?: string | null; metadata?: { key: string; value: string }[]; + tags?: { key: string; value: string }[]; }; export enum DynamicSecretProviders { @@ -35,7 +36,8 @@ export enum DynamicSecretProviders { SapAse = "sap-ase", Kubernetes = "kubernetes", Vertica = "vertica", - GcpIam = "gcp-iam" + GcpIam = "gcp-iam", + Github = "github" } export enum KubernetesDynamicSecretCredentialType { @@ -89,6 +91,7 @@ export type TDynamicSecretProvider = } | { type: DynamicSecretProviders.AwsIam; + tags?: { key: string; value: string }[]; inputs: | { method: DynamicSecretAwsIamAuth.AccessKey; @@ -333,6 +336,14 @@ export type TDynamicSecretProvider = inputs: { serviceAccountEmail: string; }; + } + | { + type: DynamicSecretProviders.Github; + inputs: { + appId: number; + installationId: number; + privateKey: string; + }; }; export type TCreateDynamicSecretDTO = { @@ -345,6 +356,7 @@ export type TCreateDynamicSecretDTO = { name: string; metadata?: { key: string; value: string }[]; usernameTemplate?: string; + tags?: { key: string; value: string }[]; }; export type TUpdateDynamicSecretDTO = { @@ -359,6 +371,7 @@ export type TUpdateDynamicSecretDTO = { maxTTL?: string | null; inputs?: unknown; usernameTemplate?: string | null; + tags?: { key: string; value: string }[]; }; }; diff --git a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx index e6ba62a6b..e96dcf34f 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx +++ b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-param-reassign */ -import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query"; +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { decryptAssymmetric, @@ -25,10 +25,11 @@ export const secretApprovalRequestKeys = { status, committer, offset, - limit + limit, + search }: TGetSecretApprovalRequestList) => [ - { workspaceId, environment, status, committer, offset, limit }, + { workspaceId, environment, status, committer, offset, limit, search }, "secret-approval-requests" ] as const, detail: ({ id }: Omit) => @@ -118,23 +119,25 @@ const fetchSecretApprovalRequestList = async ({ committer, status = "open", limit = 20, - offset + offset = 0, + search = "" }: TGetSecretApprovalRequestList) => { - const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequest[] }>( - "/api/v1/secret-approval-requests", - { - params: { - workspaceId, - environment, - committer, - status, - limit, - offset - } + const { data } = await apiRequest.get<{ + approvals: TSecretApprovalRequest[]; + totalCount: number; + }>("/api/v1/secret-approval-requests", { + params: { + workspaceId, + environment, + committer, + status, + limit, + offset, + search } - ); + }); - return data.approvals; + return data; }; export const useGetSecretApprovalRequests = ({ @@ -143,31 +146,32 @@ export const useGetSecretApprovalRequests = ({ options = {}, status, limit = 20, + offset = 0, + search, committer }: TGetSecretApprovalRequestList & TReactQueryOptions) => - useInfiniteQuery({ - initialPageParam: 0, + useQuery({ queryKey: secretApprovalRequestKeys.list({ workspaceId, environment, committer, - status + status, + limit, + search, + offset }), - queryFn: ({ pageParam }) => + queryFn: () => fetchSecretApprovalRequestList({ workspaceId, environment, status, committer, limit, - offset: pageParam + offset, + search }), enabled: Boolean(workspaceId) && (options?.enabled ?? true), - getNextPageParam: (lastPage, pages) => { - if (lastPage.length && lastPage.length < limit) return undefined; - - return lastPage?.length !== 0 ? pages.length * limit : undefined; - } + placeholderData: (previousData) => previousData }); const fetchSecretApprovalRequestDetails = async ({ diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts index 18360377f..3983d325a 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/types.ts +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -113,6 +113,7 @@ export type TGetSecretApprovalRequestList = { committer?: string; limit?: number; offset?: number; + search?: string; }; export type TGetSecretApprovalRequestCount = { diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index bfd45d541..66834ced5 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -19,7 +19,8 @@ export enum SecretSync { Heroku = "heroku", Render = "render", Flyio = "flyio", - GitLab = "gitlab" + GitLab = "gitlab", + CloudflarePages = "cloudflare-pages" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/cloudflare-pages-sync.ts b/frontend/src/hooks/api/secretSyncs/types/cloudflare-pages-sync.ts new file mode 100644 index 000000000..14fc268b7 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/cloudflare-pages-sync.ts @@ -0,0 +1,16 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TCloudflarePagesSync = TRootSecretSync & { + destination: SecretSync.CloudflarePages; + destinationConfig: { + projectName: string; + environment: string; + }; + connection: { + app: AppConnection.Cloudflare; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 48921b408..0d457d543 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -9,6 +9,7 @@ import { TAzureAppConfigurationSync } from "./azure-app-configuration-sync"; import { TAzureDevOpsSync } from "./azure-devops-sync"; import { TAzureKeyVaultSync } from "./azure-key-vault-sync"; import { TCamundaSync } from "./camunda-sync"; +import { TCloudflarePagesSync } from "./cloudflare-pages-sync"; import { TDatabricksSync } from "./databricks-sync"; import { TFlyioSync } from "./flyio-sync"; import { TGcpSync } from "./gcp-sync"; @@ -51,7 +52,8 @@ export type TSecretSync = | THerokuSync | TRenderSync | TFlyioSync - | TGitlabSync; + | TGitlabSync + | TCloudflarePagesSync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx index 3711b7632..86fc39300 100644 --- a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx +++ b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx @@ -352,9 +352,9 @@ export const ProjectLayout = () => { secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount ) && ( - + {pendingRequestsCount} - + )} )} diff --git a/frontend/src/pages/admin/IntegrationsPage/components/GitHubAppConnectionForm.tsx b/frontend/src/pages/admin/IntegrationsPage/components/GitHubAppConnectionForm.tsx new file mode 100644 index 000000000..30af87db3 --- /dev/null +++ b/frontend/src/pages/admin/IntegrationsPage/components/GitHubAppConnectionForm.tsx @@ -0,0 +1,222 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { FaGithub } from "react-icons/fa"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Button, + FormControl, + Input, + TextArea +} from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { useUpdateServerConfig } from "@app/hooks/api"; +import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types"; + +const gitHubAppFormSchema = z.object({ + clientId: z.string(), + clientSecret: z.string(), + appSlug: z.string(), + appId: z.string(), + privateKey: z.string() +}); + +type TGitHubAppConnectionForm = z.infer; + +type Props = { + adminIntegrationsConfig?: AdminIntegrationsConfig; +}; + +export const GitHubAppConnectionForm = ({ adminIntegrationsConfig }: Props) => { + const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig(); + const [isGitHubAppClientSecretFocused, setIsGitHubAppClientSecretFocused] = useToggle(); + const { + control, + handleSubmit, + setValue, + formState: { isSubmitting, isDirty } + } = useForm({ + resolver: zodResolver(gitHubAppFormSchema) + }); + + const onSubmit = async (data: TGitHubAppConnectionForm) => { + await updateAdminServerConfig({ + gitHubAppConnectionClientId: data.clientId, + gitHubAppConnectionClientSecret: data.clientSecret, + gitHubAppConnectionSlug: data.appSlug, + gitHubAppConnectionId: data.appId, + gitHubAppConnectionPrivateKey: data.privateKey + }); + + createNotification({ + text: "Updated GitHub app connection configuration. It can take up to 5 minutes to take effect.", + type: "success" + }); + }; + + useEffect(() => { + if (adminIntegrationsConfig) { + setValue("clientId", adminIntegrationsConfig.gitHubAppConnection.clientId); + setValue("clientSecret", adminIntegrationsConfig.gitHubAppConnection.clientSecret); + setValue("appSlug", adminIntegrationsConfig.gitHubAppConnection.appSlug); + setValue("appId", adminIntegrationsConfig.gitHubAppConnection.appId); + setValue("privateKey", adminIntegrationsConfig.gitHubAppConnection.privateKey); + } + }, [adminIntegrationsConfig]); + + return ( +
+ + + +
+ +
GitHub App
+
+
+ +
+
+ Step 1: Create and configure GitHub App. Please refer to the documentation below for + more information. +
+ +
+ Step 2: Configure your instance-wide settings to enable GitHub App connections. Copy + the credentials from your GitHub App's settings page. +
+ ( + + field.onChange(e.target.value)} + /> + + )} + /> + ( + + setIsGitHubAppClientSecretFocused.on()} + onBlur={() => setIsGitHubAppClientSecretFocused.off()} + onChange={(e) => field.onChange(e.target.value)} + /> + + )} + /> + + ( + + field.onChange(e.target.value)} + /> + + )} + /> + + ( + + field.onChange(e.target.value)} + /> + + )} + /> + + ( + +