diff --git a/backend/src/db/migrations/20250710153448_adjust-approval-request-user-cols.ts b/backend/src/db/migrations/20250710153448_adjust-approval-request-user-cols.ts new file mode 100644 index 000000000..6c0131317 --- /dev/null +++ b/backend/src/db/migrations/20250710153448_adjust-approval-request-user-cols.ts @@ -0,0 +1,35 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + const hasCommitterCol = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerUserId"); + + if (hasCommitterCol) { + await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => { + tb.uuid("committerUserId").nullable().alter(); + }); + } + + const hasRequesterCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "requestedByUserId"); + + if (hasRequesterCol) { + await knex.schema.alterTable(TableName.AccessApprovalRequest, (tb) => { + tb.dropForeign("requestedByUserId"); + tb.foreign("requestedByUserId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + // can't undo committer nullable + + const hasRequesterCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "requestedByUserId"); + + if (hasRequesterCol) { + await knex.schema.alterTable(TableName.AccessApprovalRequest, (tb) => { + tb.dropForeign("requestedByUserId"); + tb.foreign("requestedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + }); + } +} diff --git a/backend/src/db/migrations/20250711005900_github-app-connection-to-environments.ts b/backend/src/db/migrations/20250711005900_github-app-connection-to-environments.ts new file mode 100644 index 000000000..14e9ddd65 --- /dev/null +++ b/backend/src/db/migrations/20250711005900_github-app-connection-to-environments.ts @@ -0,0 +1,66 @@ +import { Knex } from "knex"; + +import { inMemoryKeyStore } from "@app/keystore/memory"; +import { selectAllTableCols } from "@app/lib/knex"; + +import { TableName } from "../schemas"; +import { getMigrationEnvConfig } from "./utils/env-config"; +import { getMigrationEncryptionServices } from "./utils/services"; + +export async function up(knex: Knex) { + const existingSuperAdminsWithGithubConnection = await knex(TableName.SuperAdmin) + .select(selectAllTableCols(TableName.SuperAdmin)) + .whereNotNull(`${TableName.SuperAdmin}.encryptedGitHubAppConnectionClientId`); + + const envConfig = getMigrationEnvConfig(); + const keyStore = inMemoryKeyStore(); + const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex }); + + const decryptor = kmsService.decryptWithRootKey(); + const encryptor = kmsService.encryptWithRootKey(); + + const tasks = existingSuperAdminsWithGithubConnection.map(async (admin) => { + const overrides = ( + admin.encryptedEnvOverrides ? JSON.parse(decryptor(Buffer.from(admin.encryptedEnvOverrides)).toString()) : {} + ) as Record; + + if (admin.encryptedGitHubAppConnectionClientId) { + overrides.INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID = decryptor( + admin.encryptedGitHubAppConnectionClientId + ).toString(); + } + + if (admin.encryptedGitHubAppConnectionClientSecret) { + overrides.INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET = decryptor( + admin.encryptedGitHubAppConnectionClientSecret + ).toString(); + } + + if (admin.encryptedGitHubAppConnectionPrivateKey) { + overrides.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY = decryptor( + admin.encryptedGitHubAppConnectionPrivateKey + ).toString(); + } + + if (admin.encryptedGitHubAppConnectionSlug) { + overrides.INF_APP_CONNECTION_GITHUB_APP_SLUG = decryptor(admin.encryptedGitHubAppConnectionSlug).toString(); + } + + if (admin.encryptedGitHubAppConnectionId) { + overrides.INF_APP_CONNECTION_GITHUB_APP_ID = decryptor(admin.encryptedGitHubAppConnectionId).toString(); + } + + const encryptedEnvOverrides = encryptor(Buffer.from(JSON.stringify(overrides))); + + await knex(TableName.SuperAdmin).where({ id: admin.id }).update({ + encryptedEnvOverrides + }); + }); + + await Promise.all(tasks); +} + +export async function down() { + // No down migration needed as this migration is only for data transformation + // and does not change the schema. +} diff --git a/backend/src/db/schemas/access-approval-policies-approvers.ts b/backend/src/db/schemas/access-approval-policies-approvers.ts index 7bc3a7e81..34d4dbf08 100644 --- a/backend/src/db/schemas/access-approval-policies-approvers.ts +++ b/backend/src/db/schemas/access-approval-policies-approvers.ts @@ -14,8 +14,8 @@ export const AccessApprovalPoliciesApproversSchema = z.object({ updatedAt: z.date(), approverUserId: z.string().uuid().nullable().optional(), approverGroupId: z.string().uuid().nullable().optional(), - sequence: z.number().default(0).nullable().optional(), - approvalsRequired: z.number().default(1).nullable().optional() + sequence: z.number().default(1).nullable().optional(), + approvalsRequired: z.number().nullable().optional() }); export type TAccessApprovalPoliciesApprovers = z.infer; diff --git a/backend/src/db/schemas/certificate-authorities.ts b/backend/src/db/schemas/certificate-authorities.ts index 62a2d6ceb..c631b87f0 100644 --- a/backend/src/db/schemas/certificate-authorities.ts +++ b/backend/src/db/schemas/certificate-authorities.ts @@ -12,8 +12,8 @@ export const CertificateAuthoritiesSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), projectId: z.string(), - enableDirectIssuance: z.boolean().default(true), status: z.string(), + enableDirectIssuance: z.boolean().default(true), name: z.string() }); diff --git a/backend/src/db/schemas/certificates.ts b/backend/src/db/schemas/certificates.ts index 5b832bab4..6bedf01ad 100644 --- a/backend/src/db/schemas/certificates.ts +++ b/backend/src/db/schemas/certificates.ts @@ -25,8 +25,8 @@ export const CertificatesSchema = z.object({ certificateTemplateId: z.string().uuid().nullable().optional(), keyUsages: z.string().array().nullable().optional(), extendedKeyUsages: z.string().array().nullable().optional(), - pkiSubscriberId: z.string().uuid().nullable().optional(), - projectId: z.string() + projectId: z.string(), + pkiSubscriberId: z.string().uuid().nullable().optional() }); export type TCertificates = z.infer; diff --git a/backend/src/db/schemas/secret-approval-requests.ts b/backend/src/db/schemas/secret-approval-requests.ts index 218a0f922..10e9b6eaf 100644 --- a/backend/src/db/schemas/secret-approval-requests.ts +++ b/backend/src/db/schemas/secret-approval-requests.ts @@ -18,7 +18,7 @@ export const SecretApprovalRequestsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), isReplicated: z.boolean().nullable().optional(), - committerUserId: z.string().uuid(), + committerUserId: z.string().uuid().nullable().optional(), statusChangedByUserId: z.string().uuid().nullable().optional(), bypassReason: z.string().nullable().optional() }); 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 66f6708a0..3d07af562 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -58,7 +58,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv deletedAt: z.date().nullish(), allowedSelfApprovals: z.boolean() }), - committerUser: approvalRequestUser, + committerUser: approvalRequestUser.nullish(), commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(), environment: z.string(), reviewers: z.object({ userId: z.string(), status: z.string() }).array(), @@ -308,7 +308,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv }), environment: z.string(), statusChangedByUser: approvalRequestUser.optional(), - committerUser: approvalRequestUser, + committerUser: approvalRequestUser.nullish(), reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(), secretPath: z.string(), commits: secretRawSchema diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 635bae46b..8f3f84e00 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -1711,7 +1711,7 @@ interface SecretApprovalReopened { interface SecretApprovalRequest { type: EventType.SECRET_APPROVAL_REQUEST; metadata: { - committedBy: string; + committedBy?: string | null; secretApprovalRequestSlug: string; secretApprovalRequestId: string; eventType: SecretApprovalEvent; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index 2a4f895c4..f3aac5b92 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -361,13 +361,6 @@ export const ldapConfigServiceFactory = ({ }); } else { const plan = await licenseService.getPlan(orgId); - if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { - // limit imposed on number of members allowed / number of members used exceeds the number of members allowed - throw new BadRequestError({ - message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members." - }); - } - if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed throw new BadRequestError({ diff --git a/backend/src/ee/services/license/licence-enums.ts b/backend/src/ee/services/license/licence-enums.ts index 8812621f2..340fc764f 100644 --- a/backend/src/ee/services/license/licence-enums.ts +++ b/backend/src/ee/services/license/licence-enums.ts @@ -1,5 +1,4 @@ export const BillingPlanRows = { - MemberLimit: { name: "Organization member limit", field: "memberLimit" }, IdentityLimit: { name: "Organization identity limit", field: "identityLimit" }, WorkspaceLimit: { name: "Project limit", field: "workspaceLimit" }, EnvironmentLimit: { name: "Environment limit", field: "environmentLimit" }, diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index b841c5bea..7e784d9ad 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -442,9 +442,7 @@ export const licenseServiceFactory = ({ rows: data.rows.map((el) => { let used = "-"; - if (el.name === BillingPlanRows.MemberLimit.name) { - used = orgMembersUsed.toString(); - } else if (el.name === BillingPlanRows.WorkspaceLimit.name) { + if (el.name === BillingPlanRows.WorkspaceLimit.name) { used = projectCount.toString(); } else if (el.name === BillingPlanRows.IdentityLimit.name) { used = (identityUsed + orgMembersUsed).toString(); @@ -464,12 +462,10 @@ export const licenseServiceFactory = ({ const allowed = onPremFeatures[field as keyof TFeatureSet]; let used = "-"; - if (field === BillingPlanRows.MemberLimit.field) { - used = orgMembersUsed.toString(); - } else if (field === BillingPlanRows.WorkspaceLimit.field) { + if (field === BillingPlanRows.WorkspaceLimit.field) { used = projectCount.toString(); } else if (field === BillingPlanRows.IdentityLimit.field) { - used = identityUsed.toString(); + used = (identityUsed + orgMembersUsed).toString(); } return { diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index a89bc0346..578ab469c 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -311,13 +311,6 @@ export const samlConfigServiceFactory = ({ }); } else { const plan = await licenseService.getPlan(orgId); - if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { - // limit imposed on number of members allowed / number of members used exceeds the number of members allowed - throw new BadRequestError({ - message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members." - }); - } - if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed throw new BadRequestError({ 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 ec6a17d97..c098d9b31 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 @@ -45,7 +45,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { `${TableName.SecretApprovalRequest}.statusChangedByUserId`, `statusChangedByUser.id` ) - .join( + .leftJoin( db(TableName.Users).as("committerUser"), `${TableName.SecretApprovalRequest}.committerUserId`, `committerUser.id` @@ -173,13 +173,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { username: el.statusChangedByUserUsername } : undefined, - committerUser: { - userId: el.committerUserId, - email: el.committerUserEmail, - firstName: el.committerUserFirstName, - lastName: el.committerUserLastName, - username: el.committerUserUsername - }, + committerUser: el.committerUserId + ? { + userId: el.committerUserId, + email: el.committerUserEmail, + firstName: el.committerUserFirstName, + lastName: el.committerUserLastName, + username: el.committerUserUsername + } + : null, policy: { id: el.policyId, name: el.policyName, @@ -377,7 +379,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { `${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`, `bypasserUserGroupMembership.groupId` ) - .join( + .leftJoin( db(TableName.Users).as("committerUser"), `${TableName.SecretApprovalRequest}.committerUserId`, `committerUser.id` @@ -488,13 +490,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { enforcementLevel: el.policyEnforcementLevel, allowedSelfApprovals: el.policyAllowedSelfApprovals }, - committerUser: { - userId: el.committerUserId, - email: el.committerUserEmail, - firstName: el.committerUserFirstName, - lastName: el.committerUserLastName, - username: el.committerUserUsername - } + committerUser: el.committerUserId + ? { + userId: el.committerUserId, + email: el.committerUserEmail, + firstName: el.committerUserFirstName, + lastName: el.committerUserLastName, + username: el.committerUserUsername + } + : null }), childrenMapper: [ { @@ -581,7 +585,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { `${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`, `bypasserUserGroupMembership.groupId` ) - .join( + .leftJoin( db(TableName.Users).as("committerUser"), `${TableName.SecretApprovalRequest}.committerUserId`, `committerUser.id` @@ -693,13 +697,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { enforcementLevel: el.policyEnforcementLevel, allowedSelfApprovals: el.policyAllowedSelfApprovals }, - committerUser: { - userId: el.committerUserId, - email: el.committerUserEmail, - firstName: el.committerUserFirstName, - lastName: el.committerUserLastName, - username: el.committerUserUsername - } + committerUser: el.committerUserId + ? { + userId: el.committerUserId, + email: el.committerUserEmail, + firstName: el.committerUserFirstName, + lastName: el.committerUserLastName, + username: el.committerUserUsername + } + : null }), childrenMapper: [ { 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 8b4f0b473..9cac1dd43 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 @@ -1322,7 +1322,7 @@ export const secretApprovalRequestServiceFactory = ({ }); const env = await projectEnvDAL.findOne({ id: policy.envId }); - const user = await userDAL.findById(secretApprovalRequest.committerUserId); + const user = await userDAL.findById(actorId); await triggerWorkflowIntegrationNotification({ input: { @@ -1659,7 +1659,7 @@ export const secretApprovalRequestServiceFactory = ({ return { ...doc, commits: approvalCommits }; }); - const user = await userDAL.findById(secretApprovalRequest.committerUserId); + const user = await userDAL.findById(actorId); const env = await projectEnvDAL.findOne({ id: policy.envId }); await triggerWorkflowIntegrationNotification({ diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index a61e542b1..59734583a 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2472,6 +2472,9 @@ export const SecretSyncs = { projectName: "The name of the Cloudflare Pages project to sync secrets to.", environment: "The environment of the Cloudflare Pages project to sync secrets to." }, + CLOUDFLARE_WORKERS: { + scriptId: "The ID of the Cloudflare Workers script to sync secrets to." + }, ZABBIX: { scope: "The Zabbix scope that secrets should be synced to.", hostId: "The ID of the Zabbix host to sync secrets to.", 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 index bd3507a7d..d18d0564e 100644 --- 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 @@ -50,4 +50,32 @@ export const registerCloudflareConnectionRouter = async (server: FastifyZodProvi return projects; } }); + + server.route({ + method: "GET", + url: `/:connectionId/cloudflare-workers-scripts`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const projects = await server.services.appConnection.cloudflare.listWorkersScripts(connectionId, req.permission); + + return projects; + } + }); }; diff --git a/backend/src/server/routes/v1/secret-sync-routers/cloudflare-workers-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/cloudflare-workers-sync-router.ts new file mode 100644 index 000000000..a045a5031 --- /dev/null +++ b/backend/src/server/routes/v1/secret-sync-routers/cloudflare-workers-sync-router.ts @@ -0,0 +1,17 @@ +import { + CloudflareWorkersSyncSchema, + CreateCloudflareWorkersSyncSchema, + UpdateCloudflareWorkersSyncSchema +} from "@app/services/secret-sync/cloudflare-workers/cloudflare-workers-schemas"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; + +import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; + +export const registerCloudflareWorkersSyncRouter = async (server: FastifyZodProvider) => + registerSyncSecretsEndpoints({ + destination: SecretSync.CloudflareWorkers, + server, + responseSchema: CloudflareWorkersSyncSchema, + createSchema: CreateCloudflareWorkersSyncSchema, + updateSchema: UpdateCloudflareWorkersSyncSchema + }); 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 038fce7aa..4e1d185a8 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -9,6 +9,7 @@ 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 { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerFlyioSyncRouter } from "./flyio-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; @@ -50,6 +51,8 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { @@ -43,6 +44,28 @@ export const listCloudflarePagesProjects = async ( })); }; +export const listCloudflareWorkersScripts = async ( + appConnection: TCloudflareConnection +): Promise => { + const { + credentials: { apiToken, accountId } + } = appConnection; + + const { data } = await request.get<{ result: { id: string }[] }>( + `${IntegrationUrls.CLOUDFLARE_API_URL}/client/v4/accounts/${accountId}/workers/scripts`, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + + return data.result.map((a) => ({ + id: a.id + })); +}; + export const validateCloudflareConnectionCredentials = async (config: TCloudflareConnectionConfig) => { const { apiToken, accountId } = config.credentials; diff --git a/backend/src/services/app-connection/cloudflare/cloudflare-connection-service.ts b/backend/src/services/app-connection/cloudflare/cloudflare-connection-service.ts index 2d1f38786..5a8a161fc 100644 --- a/backend/src/services/app-connection/cloudflare/cloudflare-connection-service.ts +++ b/backend/src/services/app-connection/cloudflare/cloudflare-connection-service.ts @@ -2,7 +2,7 @@ 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 { listCloudflarePagesProjects, listCloudflareWorkersScripts } from "./cloudflare-connection-fns"; import { TCloudflareConnection } from "./cloudflare-connection-types"; type TGetAppConnectionFunc = ( @@ -19,12 +19,31 @@ export const cloudflareConnectionService = (getAppConnection: TGetAppConnectionF return projects; } catch (error) { - logger.error(error, "Failed to list Cloudflare Pages projects for Cloudflare connection"); + logger.error( + error, + `Failed to list Cloudflare Pages projects for Cloudflare connection [connectionId=${connectionId}]` + ); + return []; + } + }; + + const listWorkersScripts = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Cloudflare, connectionId, actor); + try { + const projects = await listCloudflareWorkersScripts(appConnection); + + return projects; + } catch (error) { + logger.error( + error, + `Failed to list Cloudflare Workers scripts for Cloudflare connection [connectionId=${connectionId}]` + ); return []; } }; return { - listPagesProjects + listPagesProjects, + listWorkersScripts }; }; diff --git a/backend/src/services/app-connection/cloudflare/cloudflare-connection-types.ts b/backend/src/services/app-connection/cloudflare/cloudflare-connection-types.ts index 6b2ee0d04..0ac1b708c 100644 --- a/backend/src/services/app-connection/cloudflare/cloudflare-connection-types.ts +++ b/backend/src/services/app-connection/cloudflare/cloudflare-connection-types.ts @@ -28,3 +28,7 @@ export type TCloudflarePagesProject = { id: string; name: string; }; + +export type TCloudflareWorkersScript = { + id: 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 ebdd09289..d8c98e832 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -7,7 +7,6 @@ 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"; @@ -15,14 +14,13 @@ 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: gitHubAppConnection.appSlug || INF_APP_CONNECTION_GITHUB_APP_SLUG + appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG }; }; @@ -32,10 +30,9 @@ 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; + const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID; + const appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY; switch (method) { case GitHubConnectionMethod.App: @@ -157,8 +154,6 @@ 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, @@ -170,8 +165,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect const { clientId, clientSecret } = method === GitHubConnectionMethod.App ? { - clientId: gitHubAppConnection.clientId || INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID, - clientSecret: gitHubAppConnection.clientSecret || INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET + clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID, + clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET } : // oauth { diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index b6e6bf4ec..24a1682a3 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -913,14 +913,6 @@ export const orgServiceFactory = ({ // if there exist no org membership we set is as given by the request if (!inviteeOrgMembership) { - if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { - // limit imposed on number of members allowed / number of members used exceeds the number of members allowed - throw new BadRequestError({ - name: "InviteUser", - message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members." - }); - } - if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed throw new BadRequestError({ diff --git a/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-constants.ts b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-constants.ts new file mode 100644 index 000000000..459fda316 --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-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_WORKERS_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Cloudflare Workers", + destination: SecretSync.CloudflareWorkers, + connection: AppConnection.Cloudflare, + canImportSecrets: false +}; diff --git a/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-fns.ts b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-fns.ts new file mode 100644 index 000000000..98c9869f0 --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-fns.ts @@ -0,0 +1,121 @@ +import { request } from "@app/lib/config/request"; +import { applyJitter } from "@app/lib/dates"; +import { delay as delayMs } from "@app/lib/delay"; +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 { TCloudflareWorkersSyncWithCredentials } from "./cloudflare-workers-types"; + +const getSecretKeys = async (secretSync: TCloudflareWorkersSyncWithCredentials): Promise => { + const { + destinationConfig, + connection: { + credentials: { apiToken, accountId } + } + } = secretSync; + + const { data } = await request.get<{ + result: Array<{ name: string }>; + }>( + `${IntegrationUrls.CLOUDFLARE_WORKERS_API_URL}/client/v4/accounts/${accountId}/workers/scripts/${destinationConfig.scriptId}/secrets`, + { + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/json" + } + } + ); + + return data.result.map((s) => s.name); +}; + +export const CloudflareWorkersSyncFns = { + syncSecrets: async (secretSync: TCloudflareWorkersSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection: { + credentials: { apiToken, accountId } + }, + destinationConfig: { scriptId } + } = secretSync; + + const existingSecretNames = await getSecretKeys(secretSync); + const secretMapKeys = new Set(Object.keys(secretMap)); + + for await (const [key, val] of Object.entries(secretMap)) { + await delayMs(Math.max(0, applyJitter(100, 200))); + await request.put( + `${IntegrationUrls.CLOUDFLARE_WORKERS_API_URL}/client/v4/accounts/${accountId}/workers/scripts/${scriptId}/secrets`, + { name: key, text: val.value, type: "secret_text" }, + { + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json" + } + } + ); + } + + if (!secretSync.syncOptions.disableSecretDeletion) { + const secretsToDelete = existingSecretNames.filter((existingKey) => { + const isManagedBySchema = matchesSchema( + existingKey, + secretSync.environment?.slug || "", + secretSync.syncOptions.keySchema + ); + const isInNewSecretMap = secretMapKeys.has(existingKey); + return !isInNewSecretMap && isManagedBySchema; + }); + + for await (const key of secretsToDelete) { + await delayMs(Math.max(0, applyJitter(100, 200))); + await request.delete( + `${IntegrationUrls.CLOUDFLARE_WORKERS_API_URL}/client/v4/accounts/${accountId}/workers/scripts/${scriptId}/secrets/${key}`, + { + headers: { + Authorization: `Bearer ${apiToken}` + } + } + ); + } + } + }, + + getSecrets: async (secretSync: TCloudflareWorkersSyncWithCredentials): Promise => { + throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`); + }, + + removeSecrets: async (secretSync: TCloudflareWorkersSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection: { + credentials: { apiToken, accountId } + }, + destinationConfig: { scriptId } + } = secretSync; + + const existingSecretNames = await getSecretKeys(secretSync); + const secretMapToRemoveKeys = new Set(Object.keys(secretMap)); + + for await (const existingKey of existingSecretNames) { + const isManagedBySchema = matchesSchema( + existingKey, + secretSync.environment?.slug || "", + secretSync.syncOptions.keySchema + ); + const isInSecretMapToRemove = secretMapToRemoveKeys.has(existingKey); + + if (isInSecretMapToRemove && isManagedBySchema) { + await delayMs(Math.max(0, applyJitter(100, 200))); + await request.delete( + `${IntegrationUrls.CLOUDFLARE_WORKERS_API_URL}/client/v4/accounts/${accountId}/workers/scripts/${scriptId}/secrets/${existingKey}`, + { + headers: { + Authorization: `Bearer ${apiToken}` + } + } + ); + } + } + } +}; diff --git a/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-schemas.ts b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-schemas.ts new file mode 100644 index 000000000..b5698867a --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-schemas.ts @@ -0,0 +1,55 @@ +import RE2 from "re2"; +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 CloudflareWorkersSyncDestinationConfigSchema = z.object({ + scriptId: z + .string() + .min(1, "Script ID is required") + .max(64) + .refine((val) => { + const re2 = new RE2(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/); + return re2.test(val); + }, "Invalid script ID format") + .describe(SecretSyncs.DESTINATION_CONFIG.CLOUDFLARE_WORKERS.scriptId) +}); + +const CloudflareWorkersSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; + +export const CloudflareWorkersSyncSchema = BaseSecretSyncSchema( + SecretSync.CloudflareWorkers, + CloudflareWorkersSyncOptionsConfig +).extend({ + destination: z.literal(SecretSync.CloudflareWorkers), + destinationConfig: CloudflareWorkersSyncDestinationConfigSchema +}); + +export const CreateCloudflareWorkersSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.CloudflareWorkers, + CloudflareWorkersSyncOptionsConfig +).extend({ + destinationConfig: CloudflareWorkersSyncDestinationConfigSchema +}); + +export const UpdateCloudflareWorkersSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.CloudflareWorkers, + CloudflareWorkersSyncOptionsConfig +).extend({ + destinationConfig: CloudflareWorkersSyncDestinationConfigSchema.optional() +}); + +export const CloudflareWorkersSyncListItemSchema = z.object({ + name: z.literal("Cloudflare Workers"), + connection: z.literal(AppConnection.Cloudflare), + destination: z.literal(SecretSync.CloudflareWorkers), + canImportSecrets: z.literal(false) +}); diff --git a/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-types.ts b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-types.ts new file mode 100644 index 000000000..1b5adda7b --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-workers/cloudflare-workers-types.ts @@ -0,0 +1,19 @@ +import z from "zod"; + +import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types"; + +import { + CloudflareWorkersSyncListItemSchema, + CloudflareWorkersSyncSchema, + CreateCloudflareWorkersSyncSchema +} from "./cloudflare-workers-schemas"; + +export type TCloudflareWorkersSyncListItem = z.infer; + +export type TCloudflareWorkersSync = z.infer; + +export type TCloudflareWorkersSyncInput = z.infer; + +export type TCloudflareWorkersSyncWithCredentials = TCloudflareWorkersSync & { + connection: TCloudflareConnection; +}; diff --git a/backend/src/services/secret-sync/cloudflare-workers/index.ts b/backend/src/services/secret-sync/cloudflare-workers/index.ts new file mode 100644 index 000000000..5d7916a4d --- /dev/null +++ b/backend/src/services/secret-sync/cloudflare-workers/index.ts @@ -0,0 +1,4 @@ +export * from "./cloudflare-workers-constants"; +export * from "./cloudflare-workers-fns"; +export * from "./cloudflare-workers-schemas"; +export * from "./cloudflare-workers-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index c7dc0c9bb..64a191a22 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -21,6 +21,8 @@ export enum SecretSync { Flyio = "flyio", GitLab = "gitlab", CloudflarePages = "cloudflare-pages", + CloudflareWorkers = "cloudflare-workers", + Zabbix = "zabbix", Railway = "railway" } diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 34b24eece..7dc19d3e9 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -31,6 +31,7 @@ import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./az 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 { CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, CloudflareWorkersSyncFns } from "./cloudflare-workers"; import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; @@ -72,6 +73,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION, [SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION, [SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION, + [SecretSync.CloudflareWorkers]: CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, + [SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION, [SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION }; @@ -241,6 +244,8 @@ export const SecretSyncFns = { return GitLabSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); case SecretSync.CloudflarePages: return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.CloudflareWorkers: + return CloudflareWorkersSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Zabbix: return ZabbixSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Railway: @@ -337,6 +342,9 @@ export const SecretSyncFns = { case SecretSync.CloudflarePages: secretMap = await CloudflarePagesSyncFns.getSecrets(secretSync); break; + case SecretSync.CloudflareWorkers: + secretMap = await CloudflareWorkersSyncFns.getSecrets(secretSync); + break; case SecretSync.Zabbix: secretMap = await ZabbixSyncFns.getSecrets(secretSync); break; @@ -420,6 +428,8 @@ export const SecretSyncFns = { return GitLabSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); case SecretSync.CloudflarePages: return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.CloudflareWorkers: + return CloudflareWorkersSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Zabbix: return ZabbixSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Railway: diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 938679332..dd1d5b146 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -24,6 +24,8 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Flyio]: "Fly.io", [SecretSync.GitLab]: "GitLab", [SecretSync.CloudflarePages]: "Cloudflare Pages", + [SecretSync.CloudflareWorkers]: "Cloudflare Workers", + [SecretSync.Zabbix]: "Zabbix", [SecretSync.Railway]: "Railway" }; @@ -51,6 +53,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.Flyio]: AppConnection.Flyio, [SecretSync.GitLab]: AppConnection.GitLab, [SecretSync.CloudflarePages]: AppConnection.Cloudflare, + [SecretSync.CloudflareWorkers]: AppConnection.Cloudflare, + [SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Railway]: AppConnection.Railway }; @@ -78,6 +82,8 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.Flyio]: SecretSyncPlanType.Regular, [SecretSync.GitLab]: SecretSyncPlanType.Regular, [SecretSync.CloudflarePages]: SecretSyncPlanType.Regular, + [SecretSync.CloudflareWorkers]: SecretSyncPlanType.Regular, + [SecretSync.Zabbix]: SecretSyncPlanType.Regular, [SecretSync.Railway]: 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 7eaba35f2..998328b85 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -78,6 +78,12 @@ import { TCloudflarePagesSyncListItem, TCloudflarePagesSyncWithCredentials } from "./cloudflare-pages/cloudflare-pages-types"; +import { + TCloudflareWorkersSync, + TCloudflareWorkersSyncInput, + TCloudflareWorkersSyncListItem, + TCloudflareWorkersSyncWithCredentials +} from "./cloudflare-workers"; import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; import { TGitLabSync, TGitLabSyncInput, TGitLabSyncListItem, TGitLabSyncWithCredentials } from "./gitlab"; @@ -144,6 +150,7 @@ export type TSecretSync = | TFlyioSync | TGitLabSync | TCloudflarePagesSync + | TCloudflareWorkersSync | TZabbixSync | TRailwaySync; @@ -170,6 +177,7 @@ export type TSecretSyncWithCredentials = | TFlyioSyncWithCredentials | TGitLabSyncWithCredentials | TCloudflarePagesSyncWithCredentials + | TCloudflareWorkersSyncWithCredentials | TZabbixSyncWithCredentials | TRailwaySyncWithCredentials; @@ -196,6 +204,7 @@ export type TSecretSyncInput = | TFlyioSyncInput | TGitLabSyncInput | TCloudflarePagesSyncInput + | TCloudflareWorkersSyncInput | TZabbixSyncInput | TRailwaySyncInput; @@ -222,6 +231,7 @@ export type TSecretSyncListItem = | TFlyioSyncListItem | TGitLabSyncListItem | TCloudflarePagesSyncListItem + | TCloudflareWorkersSyncListItem | TZabbixSyncListItem | TRailwaySyncListItem; diff --git a/docs/Dockerfile b/docs/Dockerfile index 079972544..c048fc6dc 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -19,13 +19,17 @@ FROM node:20-alpine WORKDIR /app -RUN npm install -g mint@4.2.13 +RUN addgroup -g 1001 -S mintuser && \ + adduser -S -D -H -u 1001 -s /sbin/nologin -G mintuser mintuser && \ + npm install -g mint@4.2.13 -COPY . . +COPY --chown=mintuser:mintuser . . -COPY --from=builder /root/.mintlify /root/.mintlify -COPY --from=builder /app/docs.json /app/docs.json -COPY --from=builder /app/spec.json /app/spec.json +COPY --from=builder --chown=mintuser:mintuser /root/.mintlify /home/mintuser/.mintlify +COPY --from=builder --chown=mintuser:mintuser /app/docs.json /app/docs.json +COPY --from=builder --chown=mintuser:mintuser /app/spec.json /app/spec.json + +USER mintuser EXPOSE 3000 diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/create.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/create.mdx new file mode 100644 index 000000000..d53c8eb0d --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/cloudflare-workers" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/delete.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/delete.mdx new file mode 100644 index 000000000..919074e74 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/cloudflare-workers/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-id.mdx new file mode 100644 index 000000000..5d425e722 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/cloudflare-workers/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-name.mdx new file mode 100644 index 000000000..877b58aed --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/cloudflare-workers/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/list.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/list.mdx new file mode 100644 index 000000000..1377c2282 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/cloudflare-workers" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/remove-secrets.mdx new file mode 100644 index 000000000..6446ede42 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/cloudflare-workers/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/sync-secrets.mdx new file mode 100644 index 000000000..1980f0797 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/cloudflare-workers/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/update.mdx b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/update.mdx new file mode 100644 index 000000000..4dc511b2c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/cloudflare-workers/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/cloudflare-workers/{syncId}" +--- diff --git a/docs/changelog/overview.mdx b/docs/changelog/overview.mdx index 6d1440ff1..11a058019 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -4,6 +4,61 @@ title: "Changelog" The changelog below reflects new product developments and updates on a monthly basis. + +## July 2025 +- Improved speed performance of audit log filtering. +- Revamped password reset flow pages. +- Added support for [Bitbucket for Secret Scanning](https://infisical.com/docs/documentation/platform/secret-scanning/bitbucket). +- Released Secret Sync for [Zabbix](https://infisical.com/docs/integrations/secret-syncs/zabbix). + + + +## June 2025 +- Released Secret Sync for [1Password](https://infisical.com/docs/integrations/secret-syncs/1password), [Heroku](https://infisical.com/docs/integrations/secret-syncs/heroku), [Fly.io](https://infisical.com/docs/integrations/secret-syncs/flyio), and [Render](https://infisical.com/docs/integrations/secret-syncs/render). +- Added support for [Kubernetes dynamic secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/kubernetes) to generate service account tokens +- Released Secret Rotation for [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql-credentials) and [OracleDB](https://infisical.com/docs/documentation/platform/secret-rotation/oracledb-credentials) as well as Dynamic Secrets for [Vertica](https://infisical.com/docs/documentation/platform/dynamic-secrets/vertica) and [GitHub App Tokens](https://infisical.com/docs/documentation/platform/dynamic-secrets/github). +- Added support for Azure Auth in ESO. +- [Kubernetes auth](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth) now supports gateway as a token reviewer. +- Revamped [Infisical CLI](https://infisical.com/docs/cli/commands/login) to auto-open login link. +- Rolled out [Infisical Packer integration](https://infisical.com/docs/integrations/frameworks/packer). +- Released [AliCloud Authentication method](https://infisical.com/docs/documentation/platform/identities/alicloud-auth). +- Added support for [multi-step approval workflows](https://infisical.com/docs/documentation/platform/pr-workflows). +- Revamped UI for Access Controls, Access Tree, Policies, and Approval Workflows. +- Released [TLS Certificate Authentication method](https://infisical.com/docs/documentation/platform/identities/tls-cert-auth). +- Added ability to copy session tokens in the Infisical Dashboard. +- Expanded resource support for [Infisical Terraform Provider](https://infisical.com/docs/integrations/frameworks/terraform). + + +## May 2025 +- Added support for [Microsoft Teams integration](https://infisical.com/docs/documentation/platform/workflow-integrations/microsoft-teams-integration). +- Released [Infisical Gateway](https://infisical.com/docs/documentation/platform/gateways/overview) for accessing private network resources from Infisical. +- Added support for [Host Groups](https://infisical.com/docs/documentation/platform/ssh/host-groups) in Infisical SSH. +- Updated the designs of all emails send by Infisical. +- Added secret rotation support for [Azure Client](https://infisical.com/docs/documentation/platform/secret-rotation/azure-client-secret). +- Released secret sync for [HashiCorp Vault](https://infisical.com/docs/integrations/secret-syncs/hashicorp-vault). +- Made significant improvements to [Infisical Secret Scanning](https://infisical.com/docs/documentation/platform/secret-scanning/overview). +- Released [Infisical ACME Client](https://infisical.com/docs/documentation/platform/pki/acme-ca#certificates-with-acme-ca). +- [Access requests](https://infisical.com/docs/documentation/platform/access-controls/access-requests) now support "break-glass" policies. +- Updated [Point-in-time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery) UI/UX. +- Redesigned [Approval Workflows and Change Requests](https://infisical.com/docs/documentation/platform/pr-workflows) user interface. + + +## April 2025 + +- Released ability to [request access to projects](https://infisical.com/docs/documentation/platform/access-controls/project-access-requests#project-access-requests). +- Updated UI for Audit Logs and Log Filtering. +- Launched [Infisical SSH V2](https://infisical.com/docs/documentation/platform/ssh/overview). +- Developer [Infisical MCP](https://github.com/Infisical/infisical-mcp-server). +- Added support for [Spotify Backstage Infisical plugin](https://infisical.com/docs/integrations/external/backstage). +- Added secret syncs for Terraform Cloud, Vercel, Windmill, TeamCity, and Camunda. +- Released [Auth0 Client Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/auth0-client-secret). +- Launched [Infisical C++ SDK](https://github.com/Infisical/infisical-cpp-sdk). +- Service tokens will now get expiry notifications. +- Added Infisical [Linux binary](https://infisical.com/docs/self-hosting/reference-architectures/linux-deployment-ha#linux-ha). +- Released ability to perform user impersonation. +- Added support for [LDAP password rotation](https://infisical.com/docs/documentation/platform/secret-rotation/ldap-password). + + ## March 2025 - Released [Infisical Gateway](https://infisical.com/docs/documentation/platform/gateways/overview) for secure access to private resources without needing direct inbound connections to private networks. diff --git a/docs/docs.json b/docs/docs.json index 8e6972429..3a516febb 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -514,6 +514,7 @@ "integrations/secret-syncs/azure-key-vault", "integrations/secret-syncs/camunda", "integrations/secret-syncs/cloudflare-pages", + "integrations/secret-syncs/cloudflare-workers", "integrations/secret-syncs/databricks", "integrations/secret-syncs/flyio", "integrations/secret-syncs/gcp-secret-manager", @@ -1720,6 +1721,19 @@ "api-reference/endpoints/secret-syncs/cloudflare-pages/remove-secrets" ] }, + { + "group": "Cloudflare Workers", + "pages": [ + "api-reference/endpoints/secret-syncs/cloudflare-workers/list", + "api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-id", + "api-reference/endpoints/secret-syncs/cloudflare-workers/get-by-name", + "api-reference/endpoints/secret-syncs/cloudflare-workers/create", + "api-reference/endpoints/secret-syncs/cloudflare-workers/update", + "api-reference/endpoints/secret-syncs/cloudflare-workers/delete", + "api-reference/endpoints/secret-syncs/cloudflare-workers/sync-secrets", + "api-reference/endpoints/secret-syncs/cloudflare-workers/remove-secrets" + ] + }, { "group": "Databricks", "pages": [ diff --git a/docs/images/app-connections/cloudflare/cloudflare-workers-configure-permissions.png b/docs/images/app-connections/cloudflare/cloudflare-workers-configure-permissions.png new file mode 100644 index 000000000..d752b51e3 Binary files /dev/null and b/docs/images/app-connections/cloudflare/cloudflare-workers-configure-permissions.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-created.png b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-created.png new file mode 100644 index 000000000..25c366ac9 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-created.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-destination.png b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-destination.png new file mode 100644 index 000000000..c7ccac175 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-destination.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-details.png b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-details.png new file mode 100644 index 000000000..235b6f147 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-details.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-options.png b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-options.png new file mode 100644 index 000000000..aec833bf4 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-options.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-review.png b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-review.png new file mode 100644 index 000000000..c32b164c5 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-review.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-source.png b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-source.png new file mode 100644 index 000000000..3044dbac9 Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-source.png differ diff --git a/docs/images/secret-syncs/cloudflare-workers/select-cloudflare-workers-option.png b/docs/images/secret-syncs/cloudflare-workers/select-cloudflare-workers-option.png new file mode 100644 index 000000000..a7462a3bd Binary files /dev/null and b/docs/images/secret-syncs/cloudflare-workers/select-cloudflare-workers-option.png differ diff --git a/docs/integrations/app-connections/cloudflare.mdx b/docs/integrations/app-connections/cloudflare.mdx index 66ba1256f..6d79d2567 100644 --- a/docs/integrations/app-connections/cloudflare.mdx +++ b/docs/integrations/app-connections/cloudflare.mdx @@ -35,6 +35,17 @@ Infisical supports connecting to Cloudflare using API tokens and Account ID for - **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. + + + Use the following permissions to grant Infisical access to sync secrets to Cloudflare Workers: + + ![Configure Token](/images/app-connections/cloudflare/cloudflare-workers-configure-permissions.png) + + **Required Permissions:** + - **Account** - **Workers Scripts** - **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. @@ -44,7 +55,7 @@ Infisical supports connecting to Cloudflare using API tokens and Account ID for 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) diff --git a/docs/integrations/secret-syncs/cloudflare-workers.mdx b/docs/integrations/secret-syncs/cloudflare-workers.mdx new file mode 100644 index 000000000..d6e7e25e8 --- /dev/null +++ b/docs/integrations/secret-syncs/cloudflare-workers.mdx @@ -0,0 +1,128 @@ +--- +title: "Cloudflare Workers Sync" +description: "Learn how to configure a Cloudflare Workers 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 Workers** option. + ![Select Cloudflare Workers](/images/secret-syncs/cloudflare-workers/select-cloudflare-workers-option.png) + + 3. Configure the **Source** from where secrets should be retrieved, then click **Next**. + ![Configure Source](/images/secret-syncs/cloudflare-workers/cloudflare-workers-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-workers/cloudflare-workers-sync-destination.png) + + - **Cloudflare Connection**: The Cloudflare Connection to authenticate with. + - **Cloudflare Workers Script**: Choose the Cloudflare Workers script you want to sync secrets to. + + 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + ![Configure Options](/images/secret-syncs/cloudflare-workers/cloudflare-workers-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 Workers Sync, then click **Next**. + ![Configure Details](/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + 7. Review your Cloudflare Workers Sync configuration, then click **Create Sync**. + ![Confirm Configuration](/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-review.png) + + 8. If enabled, your Cloudflare Workers Sync will begin syncing your secrets to the destination endpoint. + ![Sync Secrets](/images/secret-syncs/cloudflare-workers/cloudflare-workers-sync-created.png) + + + + To create a **Cloudflare Workers Sync**, make an API request to the [Create Cloudflare Workers Sync](/api-reference/endpoints/secret-syncs/cloudflare-workers/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/cloudflare-workers \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-cloudflare-workers-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": { + "scriptId": "my-workers-script" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "your-sync-id", + "name": "my-cloudflare-workers-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-workers", + "destinationConfig": { + "scriptId": "my-workers-script" + } + } + } + ``` + + + diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx index d19092d63..eb196c9a4 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx @@ -4,7 +4,7 @@ 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, + TCloudflarePagesProject, useCloudflareConnectionListPagesProjects } from "@app/hooks/api/appConnections/cloudflare"; import { SecretSync } from "@app/hooks/api/secretSyncs"; @@ -52,7 +52,7 @@ export const CloudflarePagesSyncFields = () => { isDisabled={!connectionId} value={projects ? (projects.find((project) => project.name === value) ?? []) : []} onChange={(option) => { - onChange((option as SingleValue)?.name ?? null); + onChange((option as SingleValue)?.name ?? null); }} options={projects} placeholder="Select a project..." diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflareWorkersSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflareWorkersSyncFields.tsx new file mode 100644 index 000000000..c8354fabc --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflareWorkersSyncFields.tsx @@ -0,0 +1,59 @@ +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 } from "@app/components/v2"; +import { + TCloudflareWorkersScript, + useCloudflareConnectionListWorkersScripts +} from "@app/hooks/api/appConnections/cloudflare"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const CloudflareWorkersSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.CloudflareWorkers } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: scripts = [], isPending: isScriptsPending } = + useCloudflareConnectionListWorkersScripts(connectionId, { + enabled: Boolean(connectionId) + }); + + return ( + <> + { + setValue("destinationConfig.scriptId", ""); + }} + /> + ( + + script.id === value) || []} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + }} + options={scripts} + placeholder="Select a worker script..." + getOptionLabel={(option) => option.id} + getOptionValue={(option) => option.id} + /> + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index f530e0a52..cc732728c 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -11,6 +11,7 @@ import { AzureDevOpsSyncFields } from "./AzureDevOpsSyncFields"; import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields"; import { CamundaSyncFields } from "./CamundaSyncFields"; import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields"; +import { CloudflareWorkersSyncFields } from "./CloudflareWorkersSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields"; import { FlyioSyncFields } from "./FlyioSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; @@ -78,6 +79,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.CloudflarePages: return ; + case SecretSync.CloudflareWorkers: + return ; case SecretSync.Zabbix: return ; case SecretSync.Railway: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 57967f130..c86f63c20 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -58,6 +58,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Flyio: case SecretSync.GitLab: case SecretSync.CloudflarePages: + case SecretSync.CloudflareWorkers: case SecretSync.Zabbix: case SecretSync.Railway: AdditionalSyncOptionsFieldsComponent = null; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/CloudflareWorkersReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/CloudflareWorkersReviewFields.tsx new file mode 100644 index 000000000..6fe0f1f6a --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/CloudflareWorkersReviewFields.tsx @@ -0,0 +1,14 @@ +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 CloudflareWorkersSyncReviewFields = () => { + const { watch } = useFormContext< + TSecretSyncForm & { destination: SecretSync.CloudflareWorkers } + >(); + const scriptId = watch("destinationConfig.scriptId"); + + return {scriptId}; +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index 3b3a9cf74..52dc7fd02 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -20,6 +20,7 @@ import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields"; import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields"; +import { CloudflareWorkersSyncReviewFields } from "./CloudflareWorkersReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; import { FlyioSyncReviewFields } from "./FlyioSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; @@ -126,6 +127,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.CloudflarePages: DestinationFieldsComponent = ; break; + case SecretSync.CloudflareWorkers: + DestinationFieldsComponent = ; + break; case SecretSync.Zabbix: DestinationFieldsComponent = ; break; diff --git a/frontend/src/components/secret-syncs/forms/schemas/cloudflare-workers-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/cloudflare-workers-sync-destination-schema.ts new file mode 100644 index 000000000..5e4c16d09 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/cloudflare-workers-sync-destination-schema.ts @@ -0,0 +1,18 @@ +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 CloudflareWorkersSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.CloudflareWorkers), + destinationConfig: z.object({ + scriptId: z + .string() + .trim() + .min(1, "Script ID is required") + .max(64) + .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "Invalid script ID format") + }) + }) +); 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 425fd2414..82a8fa2ff 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 @@ -8,6 +8,7 @@ import { AzureDevOpsSyncDestinationSchema } from "./azure-devops-sync-destinatio 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 { CloudflareWorkersSyncDestinationSchema } from "./cloudflare-workers-sync-destination-schema"; import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema"; import { FlyioSyncDestinationSchema } from "./flyio-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; @@ -48,6 +49,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ FlyioSyncDestinationSchema, GitlabSyncDestinationSchema, CloudflarePagesSyncDestinationSchema, + CloudflareWorkersSyncDestinationSchema, + ZabbixSyncDestinationSchema, RailwaySyncDestinationSchema ]); diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 3b639d701..a90699087 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -29,10 +29,6 @@ export const ROUTE_PATHS = Object.freeze({ "/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback" ) }, - SsoPage: setRoute( - "/organization/sso", - "/_authenticate/_inject-org-details/_org-layout/organization/sso" - ), SecretSharing: setRoute( "/organization/secret-sharing", "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/" diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 5821ce959..8e9bda613 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -82,6 +82,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.Flyio]: AppConnection.Flyio, [SecretSync.GitLab]: AppConnection.Gitlab, [SecretSync.CloudflarePages]: AppConnection.Cloudflare, + [SecretSync.CloudflareWorkers]: AppConnection.Cloudflare, + [SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Railway]: AppConnection.Railway }; diff --git a/frontend/src/hooks/api/appConnections/cloudflare/queries.tsx b/frontend/src/hooks/api/appConnections/cloudflare/queries.tsx index ae2efea99..f4ca87ad1 100644 --- a/frontend/src/hooks/api/appConnections/cloudflare/queries.tsx +++ b/frontend/src/hooks/api/appConnections/cloudflare/queries.tsx @@ -3,21 +3,23 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { appConnectionKeys } from "../queries"; -import { TCloudflareProject } from "./types"; +import { TCloudflarePagesProject, TCloudflareWorkersScript } from "./types"; const cloudflareConnectionKeys = { all: [...appConnectionKeys.all, "cloudflare"] as const, listPagesProjects: (connectionId: string) => - [...cloudflareConnectionKeys.all, "pages-projects", connectionId] as const + [...cloudflareConnectionKeys.all, "pages-projects", connectionId] as const, + listWorkersScripts: (connectionId: string) => + [...cloudflareConnectionKeys.all, "workers-scripts", connectionId] as const }; export const useCloudflareConnectionListPagesProjects = ( connectionId: string, options?: Omit< UseQueryOptions< - TCloudflareProject[], + TCloudflarePagesProject[], unknown, - TCloudflareProject[], + TCloudflarePagesProject[], ReturnType >, "queryKey" | "queryFn" @@ -26,7 +28,7 @@ export const useCloudflareConnectionListPagesProjects = ( return useQuery({ queryKey: cloudflareConnectionKeys.listPagesProjects(connectionId), queryFn: async () => { - const { data } = await apiRequest.get( + const { data } = await apiRequest.get( `/api/v1/app-connections/cloudflare/${connectionId}/cloudflare-pages-projects` ); @@ -35,3 +37,28 @@ export const useCloudflareConnectionListPagesProjects = ( ...options }); }; + +export const useCloudflareConnectionListWorkersScripts = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TCloudflareWorkersScript[], + unknown, + TCloudflareWorkersScript[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: cloudflareConnectionKeys.listWorkersScripts(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/cloudflare/${connectionId}/cloudflare-workers-scripts` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/cloudflare/types.ts b/frontend/src/hooks/api/appConnections/cloudflare/types.ts index ed7bda478..1d5ea53d4 100644 --- a/frontend/src/hooks/api/appConnections/cloudflare/types.ts +++ b/frontend/src/hooks/api/appConnections/cloudflare/types.ts @@ -1,4 +1,8 @@ -export type TCloudflareProject = { +export type TCloudflarePagesProject = { id: string; name: string; }; + +export type TCloudflareWorkersScript = { + id: string; +}; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index dfbf574e8..55aaee6ef 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { organizationKeys } from "../organization/queries"; +import { subscriptionQueryKeys } from "../subscriptions/queries"; import { identitiesKeys } from "./queries"; import { AddIdentityAliCloudAuthDTO, @@ -82,6 +83,9 @@ export const useCreateIdentity = () => { queryClient.invalidateQueries({ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) }); + queryClient.invalidateQueries({ + queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId) + }); } }); }; @@ -123,6 +127,9 @@ export const useDeleteIdentity = () => { queryClient.invalidateQueries({ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) }); + queryClient.invalidateQueries({ + queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId) + }); } }); }; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index a6ba1fd40..c549ba351 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -21,6 +21,8 @@ export enum SecretSync { Flyio = "flyio", GitLab = "gitlab", CloudflarePages = "cloudflare-pages", + CloudflareWorkers = "cloudflare-workers", + Zabbix = "zabbix", Railway = "railway" } diff --git a/frontend/src/hooks/api/secretSyncs/types/cloudflare-workers-sync.ts b/frontend/src/hooks/api/secretSyncs/types/cloudflare-workers-sync.ts new file mode 100644 index 000000000..224fe8938 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/cloudflare-workers-sync.ts @@ -0,0 +1,15 @@ +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 TCloudflareWorkersSync = TRootSecretSync & { + destination: SecretSync.CloudflareWorkers; + destinationConfig: { + scriptId: 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 7071576bf..dd300c0df 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -10,6 +10,7 @@ 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 { TCloudflareWorkersSync } from "./cloudflare-workers-sync"; import { TDatabricksSync } from "./databricks-sync"; import { TFlyioSync } from "./flyio-sync"; import { TGcpSync } from "./gcp-sync"; @@ -56,6 +57,7 @@ export type TSecretSync = | TFlyioSync | TGitLabSync | TCloudflarePagesSync + | TCloudflareWorkersSync | TZabbixSync | TRailwaySync; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index ea451db02..b740b58ee 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -9,6 +9,7 @@ import { APIKeyDataV2 } from "../apiKeys/types"; import { MfaMethod } from "../auth/types"; import { TGroupWithProjectMemberships } from "../groups/types"; import { setAuthToken } from "../reactQuery"; +import { subscriptionQueryKeys } from "../subscriptions/queries"; import { workspaceKeys } from "../workspace"; import { userKeys } from "./query-keys"; import { @@ -188,6 +189,9 @@ export const useAddUsersToOrg = () => { }, onSuccess: (_, { organizationId, projects }) => { queryClient.invalidateQueries({ queryKey: userKeys.getOrgUsers(organizationId) }); + queryClient.invalidateQueries({ + queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId) + }); projects?.forEach((project) => { if (project.slug) { diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx index 89da0e2de..0bab1fcb5 100644 --- a/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/OrgSidebar/OrgSidebar.tsx @@ -1,6 +1,5 @@ import { faBook, - faCheckCircle, faCog, faCubes, faDoorClosed, @@ -100,18 +99,6 @@ export const OrgSidebar = ({ isHidden }: Props) => { )} - - {({ isActive }) => ( - -
-
- -
- SSO Settings -
-
- )} - {({ isActive }) => ( diff --git a/frontend/src/pages/admin/IntegrationsPage/components/GitHubAppConnectionForm.tsx b/frontend/src/pages/admin/IntegrationsPage/components/GitHubAppConnectionForm.tsx deleted file mode 100644 index 30af87db3..000000000 --- a/frontend/src/pages/admin/IntegrationsPage/components/GitHubAppConnectionForm.tsx +++ /dev/null @@ -1,222 +0,0 @@ -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)} - /> - - )} - /> - - ( - -