Compare commits

..

67 Commits

Author SHA1 Message Date
00fd44b33a fix type issues 2024-06-11 00:34:54 -04:00
e2550d70b5 make skipMultilineEncoding null|undefined|bool 2024-06-11 00:28:55 -04:00
163d33509b Patch multi line encoding when expandSecretRef is enabled
By default when you create a secret, it will have multi line encoding off but we actually treat this as true in the backend and UI. User’s aren’t expecting their multi line secrets to be double quoted by and made into a single line with/n, however we are doing it by default at the moment. This PR makes multi line encoding opt in and not opt out
2024-06-10 23:42:07 -04:00
b31d2be3f3 Merge pull request #1931 from minuchi/fix/preserve-annotations-when-updating
fix: preserve existing annotations when updating managed secret
2024-06-10 17:14:29 -04:00
ea517bc199 Merge pull request #1941 from belikedeep/docker-compose-command-update
Fix: Updated docker compose command (from compose V1 to compose v2)
2024-06-10 16:46:10 -04:00
a82b813553 describe isReplication flag 2024-06-10 16:20:29 -04:00
af03f706ba Merge pull request #1943 from Infisical/create-pull-request/patch-1718043322
GH Action: rename new migration file timestamp
2024-06-10 14:15:55 -04:00
9cf5bbc5d5 chore: renamed new migration files to latest timestamp (gh-action) 2024-06-10 18:15:21 +00:00
9161dd5e13 Merge pull request #1925 from Infisical/feat/add-captcha
feat: added captcha to password login
2024-06-10 14:14:57 -04:00
9303124f5f Updated docker compose command (from compose V1 to compose v2) 2024-06-10 20:09:08 +05:30
03c9a5606b Merge pull request #1928 from Infisical/feat/add-option-for-delete-disabling-github-integ
feat: added option for disabling github secret deletion
2024-06-10 22:35:04 +08:00
f4a1a00b59 misc: improved text of github option 2024-06-10 14:00:56 +08:00
b9933d711c misc: addressed schema update 2024-06-10 13:58:40 +08:00
1abdb531d9 misc: removed comments 2024-06-10 13:36:53 +08:00
59b3123eb3 adjustment: removed unintended yaml updates 2024-06-10 13:33:42 +08:00
c1954a6386 Merge branch 'feat/add-captcha' of https://github.com/Infisical/infisical into feat/add-captcha 2024-06-10 13:27:31 +08:00
0bbb86ee2a misc: simplified captcha flag and finalized build process 2024-06-10 13:24:44 +08:00
429b2a284d Merge branch 'main' into feat/add-captcha 2024-06-09 17:44:55 -04:00
6c596092b0 Merge pull request #1927 from Infisical/shubham/eng-983-optimise-secretinput-usage-to-mask-secret-when-not-in-focus
fix: share secret input now masks value onBlur
2024-06-09 17:43:30 -04:00
fcd13eac8a update saml org slug environment 2024-06-09 14:41:23 -04:00
1fb653754c update saml slug env 2024-06-09 14:37:13 -04:00
bb1d73b0f5 Merge pull request #1935 from Infisical/fix-saml-auto-redirect
patch saml auto redirect
2024-06-09 23:09:29 +05:30
59e9226d85 patch saml auto redirect 2024-06-09 13:30:44 -04:00
e6f42e1231 Merge pull request #1933 from Infisical/add-folder-sorting
added sorting for folders in overview
2024-06-08 22:31:07 -04:00
06e7a90a44 added sorting for folders in overview 2024-06-08 22:26:49 -04:00
f075ff23a9 patch encoding type for kms 2024-06-08 18:38:25 -04:00
6d40d951c6 fix: preserve existing annotations when updating managed secret 2024-06-09 01:43:41 +09:00
e5b7ebbabf revert: change in core component 2024-06-08 05:47:47 +05:30
610dd07a57 misc: updated failed password attempt limit for captcha 2024-06-08 00:39:14 +08:00
9d6d7540dc misc: removed unnecessary project property 2024-06-08 00:34:33 +08:00
847c2c67ec adjustment: made secret-deletion opt in 2024-06-08 00:30:30 +08:00
faa1572faf update docs grammar 2024-06-07 12:04:21 -04:00
d288bcbd74 explain how auto reload works in docs 2024-06-07 12:01:59 -04:00
af1d30a49a Merge pull request #1926 from Infisical/misc/added-auto-redeploy-cf-pages-option
misc: added auto-redeploy option for cf pages integration
2024-06-07 09:58:06 -04:00
76a424dcfb feat: added option for disabling github secret deletion 2024-06-07 19:00:51 +08:00
9d46c269d4 fix: secret input on tab moves to next field and masks value 2024-06-07 13:05:04 +05:30
cd92ce627c misc: added autoredeploy option for cf pages integration 2024-06-07 13:23:46 +08:00
15c05b4910 misc: finalized captcha error message 2024-06-06 21:54:35 +08:00
65d88ef08e misc: improved ux by requiring captcha entry before submission 2024-06-06 21:24:25 +08:00
81e4129e51 feat: added base captcha implementation 2024-06-06 20:42:54 +08:00
c1ca2a6f8c Merge pull request #1918 from Infisical/feat/added-rundeck-api-integration
feat: added rundeck integration
2024-06-06 12:34:59 +08:00
9b6602a8e9 Merge pull request #1917 from akhilmhdh/feat/internal-kms
Internal functions for KMS
2024-06-06 00:01:27 -04:00
=
22db286dda fix: resolved failing github action for api breaking change 2024-06-06 01:25:38 +05:30
9fd0373e39 misc: improved error handling 2024-06-06 03:15:57 +08:00
=
62f92b0bfa feat: resolved root key failing for base64 and also added projectid and orgid field 2024-06-05 23:58:45 +05:30
=
abbef4fc44 feat: changed encrypt function input to buffer 2024-06-05 22:44:21 +05:30
=
34ca942f9d fix: resolved failing test setup 2024-06-05 22:44:21 +05:30
=
1acf25dd53 feat: implmented first internal functions for KMS service 2024-06-05 22:44:20 +05:30
=
a0653883b6 feat: added pg bytea support to generate schema function 2024-06-05 22:44:20 +05:30
f3a4c32e3a Merge pull request #1921 from akhilmhdh/fix/select-list-overflow
fix: ui issue resolved for list overflow
2024-06-04 20:38:40 -04:00
6a6fe3e202 misc: finalized doc reference 2024-06-04 22:37:45 +08:00
8f4963839e misc: final updates 2024-06-04 22:34:26 +08:00
4c06f134fb Merge pull request #1884 from Infisical/fix/resolved-trailing-slash-issue-additional-privileges
fix: resolved trailing slash issue with additional privileges
2024-06-04 19:59:40 +05:30
12d3632a03 misc: added documentation for rundeck integration 2024-06-04 22:04:12 +08:00
=
c34c13887a fix: ui issue resolved for list overflow 2024-06-04 19:27:25 +05:30
378d6c259b feat: finished integration sync for rundeck 2024-06-04 21:10:24 +08:00
2a538d9560 feat: added frontend pages for rundeck integration 2024-06-04 13:30:50 +08:00
eafa50747b increase rate limits 2024-06-03 13:55:59 -04:00
77f794e1d0 Merge pull request #1891 from Infisical/snyk-fix-d845d74692da524b59866cf430937d1d
[Snyk] Security upgrade mysql2 from 3.9.7 to 3.9.8
2024-06-03 00:23:25 -04:00
3b9afb8b5b Merge pull request #1914 from Infisical/shubham/optimise-loading-gif
chore: compress loader gifs
2024-06-03 00:10:03 -04:00
8bf763dd5a Merge pull request #1903 from Infisical/feat/add-tags-to-existing-aws-params
feat: added support for tagging existing aws params
2024-06-03 01:39:17 +08:00
e93b465004 chore: compress loader gifs 2024-06-01 21:46:03 +05:30
000d87075b Merge pull request #1913 from srevinsaju/patch-1 2024-06-01 09:45:21 -04:00
2291bdc036 docs: fix typo 2024-06-01 16:22:59 +03:00
d79d7ca5e8 feat: added support for tagging existing aws params 2024-05-30 21:40:09 +08:00
f11c2d6b3e fix: backend/package.json & backend/package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-MYSQL2-6861580
2024-05-29 00:10:15 +00:00
3e9ce79398 fix: resolved trailing slash issue with additional privileges 2024-05-28 12:38:57 +08:00
89 changed files with 1437 additions and 244 deletions

View File

@ -63,3 +63,7 @@ CLIENT_SECRET_GITHUB_LOGIN=
CLIENT_ID_GITLAB_LOGIN=
CLIENT_SECRET_GITLAB_LOGIN=
CAPTCHA_SECRET=
NEXT_PUBLIC_CAPTCHA_SITE_KEY=

View File

@ -40,13 +40,14 @@ jobs:
REDIS_URL: redis://172.17.0.1:6379
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
JWT_AUTH_SECRET: something-random
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
- uses: actions/setup-go@v5
with:
go-version: '1.21.5'
- name: Wait for container to be stable and check logs
run: |
SECONDS=0
HEALTHY=0
r HEALTHY=0
while [ $SECONDS -lt 60 ]; do
if docker ps | grep infisical-api | grep -q healthy; then
echo "Container is healthy."
@ -73,4 +74,4 @@ jobs:
run: |
docker-compose -f "docker-compose.dev.yml" down
docker stop infisical-api
docker remove infisical-api
docker remove infisical-api

View File

@ -1,7 +1,7 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id
ARG SAML_ORG_SLUG=saml-org-slug-default
ARG CAPTCHA_SITE_KEY=captcha-site-key
FROM node:20-alpine AS base
@ -36,8 +36,8 @@ ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG SAML_ORG_SLUG
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
# Build
RUN npm run build
@ -113,9 +113,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
ARG SAML_ORG_SLUG
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \
BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
WORKDIR /

View File

@ -85,13 +85,13 @@ To set up and run Infisical locally, make sure you have Git and Docker installed
Linux/macOS:
```console
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.prod.yml up
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker compose -f docker-compose.prod.yml up
```
Windows Command Prompt:
```console
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.prod.yml up
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker compose -f docker-compose.prod.yml up
```
Create an account at `http://localhost:80`

View File

@ -1,4 +1,5 @@
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { Lock } from "@app/lib/red-lock";
export const mockKeyStore = (): TKeyStoreFactory => {
const store: Record<string, string | number | Buffer> = {};
@ -27,7 +28,10 @@ export const mockKeyStore = (): TKeyStoreFactory => {
return 1;
},
acquireLock: () => {
throw new Error("Not implemented");
}
return Promise.resolve({
release: () => {}
}) as Promise<Lock>;
},
waitTillReady: async () => {}
};
};

View File

@ -51,7 +51,7 @@
"libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0",
"ms": "^2.1.3",
"mysql2": "^3.9.7",
"mysql2": "^3.9.8",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.9",
"ora": "^7.0.1",
@ -10290,9 +10290,10 @@
}
},
"node_modules/mysql2": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
"integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
"version": "3.9.8",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.8.tgz",
"integrity": "sha512-+5JKNjPuks1FNMoy9TYpl77f+5frbTklz7eb3XDwbpsERRLEeXiW2PDEkakYF50UuKU2qwfGnyXpKYvukv8mGA==",
"license": "MIT",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",

View File

@ -112,7 +112,7 @@
"libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0",
"ms": "^2.1.3",
"mysql2": "^3.9.7",
"mysql2": "^3.9.8",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.9",
"ora": "^7.0.1",

View File

@ -35,6 +35,8 @@ const getZodPrimitiveType = (type: string) => {
return "z.coerce.number()";
case "text":
return "z.string()";
case "bytea":
return "zodBuffer";
default:
throw new Error(`Invalid type: ${type}`);
}
@ -96,10 +98,15 @@ const main = async () => {
const columnNames = Object.keys(columns);
let schema = "";
const zodImportSet = new Set<string>();
for (let colNum = 0; colNum < columnNames.length; colNum++) {
const columnName = columnNames[colNum];
const colInfo = columns[columnName];
let ztype = getZodPrimitiveType(colInfo.type);
if (["zodBuffer"].includes(ztype)) {
zodImportSet.add(ztype);
}
// don't put optional on id
if (colInfo.defaultValue && columnName !== "id") {
const { defaultValue } = colInfo;
@ -121,6 +128,8 @@ const main = async () => {
.split("_")
.reduce((prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`, "");
const zodImports = Array.from(zodImportSet);
// the insert and update are changed to zod input type to use default cases
writeFileSync(
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
@ -131,6 +140,8 @@ const main = async () => {
import { z } from "zod";
${zodImports.length ? `import { ${zodImports.join(",")} } from \"@app/lib/zod\";` : ""}
import { TImmutableDBKeys } from "./models";
export const ${pascalCase}Schema = z.object({${schema}});

View File

@ -98,6 +98,15 @@ import {
TIntegrations,
TIntegrationsInsert,
TIntegrationsUpdate,
TKmsKeys,
TKmsKeysInsert,
TKmsKeysUpdate,
TKmsKeyVersions,
TKmsKeyVersionsInsert,
TKmsKeyVersionsUpdate,
TKmsRootConfig,
TKmsRootConfigInsert,
TKmsRootConfigUpdate,
TLdapConfigs,
TLdapConfigsInsert,
TLdapConfigsUpdate,
@ -176,6 +185,9 @@ import {
TSecretImports,
TSecretImportsInsert,
TSecretImportsUpdate,
TSecretReferences,
TSecretReferencesInsert,
TSecretReferencesUpdate,
TSecretRotationOutputs,
TSecretRotationOutputsInsert,
TSecretRotationOutputsUpdate,
@ -240,7 +252,6 @@ import {
TWebhooksInsert,
TWebhooksUpdate
} from "@app/db/schemas";
import { TSecretReferences, TSecretReferencesInsert, TSecretReferencesUpdate } from "@app/db/schemas/secret-references";
declare module "knex/types/tables" {
interface Tables {
@ -514,5 +525,13 @@ declare module "knex/types/tables" {
TSecretVersionTagJunctionInsert,
TSecretVersionTagJunctionUpdate
>;
// KMS service
[TableName.KmsServerRootConfig]: Knex.CompositeTableType<
TKmsRootConfig,
TKmsRootConfigInsert,
TKmsRootConfigUpdate
>;
[TableName.KmsKey]: Knex.CompositeTableType<TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate>;
[TableName.KmsKeyVersion]: Knex.CompositeTableType<TKmsKeyVersions, TKmsKeyVersionsInsert, TKmsKeyVersionsUpdate>;
}
}

View File

@ -0,0 +1,56 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.KmsServerRootConfig))) {
await knex.schema.createTable(TableName.KmsServerRootConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.binary("encryptedRootKey").notNullable();
});
}
await createOnUpdateTrigger(knex, TableName.KmsServerRootConfig);
if (!(await knex.schema.hasTable(TableName.KmsKey))) {
await knex.schema.createTable(TableName.KmsKey, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.binary("encryptedKey").notNullable();
t.string("encryptionAlgorithm").notNullable();
t.integer("version").defaultTo(1).notNullable();
t.string("description");
t.boolean("isDisabled").defaultTo(false);
t.boolean("isReserved").defaultTo(true);
t.string("projectId");
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("orgId");
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
});
}
await createOnUpdateTrigger(knex, TableName.KmsKey);
if (!(await knex.schema.hasTable(TableName.KmsKeyVersion))) {
await knex.schema.createTable(TableName.KmsKeyVersion, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.binary("encryptedKey").notNullable();
t.integer("version").notNullable();
t.uuid("kmsKeyId").notNullable();
t.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE");
});
}
await createOnUpdateTrigger(knex, TableName.KmsKeyVersion);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.KmsServerRootConfig);
await dropOnUpdateTrigger(knex, TableName.KmsServerRootConfig);
await knex.schema.dropTableIfExists(TableName.KmsKeyVersion);
await dropOnUpdateTrigger(knex, TableName.KmsKeyVersion);
await knex.schema.dropTableIfExists(TableName.KmsKey);
await dropOnUpdateTrigger(knex, TableName.KmsKey);
}

View File

@ -0,0 +1,29 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasConsecutiveFailedPasswordAttempts = await knex.schema.hasColumn(
TableName.Users,
"consecutiveFailedPasswordAttempts"
);
await knex.schema.alterTable(TableName.Users, (tb) => {
if (!hasConsecutiveFailedPasswordAttempts) {
tb.integer("consecutiveFailedPasswordAttempts").defaultTo(0);
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasConsecutiveFailedPasswordAttempts = await knex.schema.hasColumn(
TableName.Users,
"consecutiveFailedPasswordAttempts"
);
await knex.schema.alterTable(TableName.Users, (tb) => {
if (hasConsecutiveFailedPasswordAttempts) {
tb.dropColumn("consecutiveFailedPasswordAttempts");
}
});
}

View File

@ -30,6 +30,9 @@ export * from "./identity-universal-auths";
export * from "./incident-contacts";
export * from "./integration-auths";
export * from "./integrations";
export * from "./kms-key-versions";
export * from "./kms-keys";
export * from "./kms-root-config";
export * from "./ldap-configs";
export * from "./ldap-group-maps";
export * from "./models";
@ -57,6 +60,7 @@ export * from "./secret-blind-indexes";
export * from "./secret-folder-versions";
export * from "./secret-folders";
export * from "./secret-imports";
export * from "./secret-references";
export * from "./secret-rotation-outputs";
export * from "./secret-rotations";
export * from "./secret-scanning-git-risks";

View File

@ -0,0 +1,21 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const KmsKeyVersionsSchema = z.object({
id: z.string().uuid(),
encryptedKey: zodBuffer,
version: z.number(),
kmsKeyId: z.string().uuid()
});
export type TKmsKeyVersions = z.infer<typeof KmsKeyVersionsSchema>;
export type TKmsKeyVersionsInsert = Omit<z.input<typeof KmsKeyVersionsSchema>, TImmutableDBKeys>;
export type TKmsKeyVersionsUpdate = Partial<Omit<z.input<typeof KmsKeyVersionsSchema>, TImmutableDBKeys>>;

View File

@ -0,0 +1,26 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const KmsKeysSchema = z.object({
id: z.string().uuid(),
encryptedKey: zodBuffer,
encryptionAlgorithm: z.string(),
version: z.number().default(1),
description: z.string().nullable().optional(),
isDisabled: z.boolean().default(false).nullable().optional(),
isReserved: z.boolean().default(true).nullable().optional(),
projectId: z.string().nullable().optional(),
orgId: z.string().uuid().nullable().optional()
});
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
export type TKmsKeysInsert = Omit<z.input<typeof KmsKeysSchema>, TImmutableDBKeys>;
export type TKmsKeysUpdate = Partial<Omit<z.input<typeof KmsKeysSchema>, TImmutableDBKeys>>;

View File

@ -0,0 +1,19 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const KmsRootConfigSchema = z.object({
id: z.string().uuid(),
encryptedRootKey: zodBuffer
});
export type TKmsRootConfig = z.infer<typeof KmsRootConfigSchema>;
export type TKmsRootConfigInsert = Omit<z.input<typeof KmsRootConfigSchema>, TImmutableDBKeys>;
export type TKmsRootConfigUpdate = Partial<Omit<z.input<typeof KmsRootConfigSchema>, TImmutableDBKeys>>;

View File

@ -81,7 +81,11 @@ export enum TableName {
DynamicSecretLease = "dynamic_secret_leases",
// junction tables with tags
JnSecretTag = "secret_tag_junction",
SecretVersionTag = "secret_version_tag_junction"
SecretVersionTag = "secret_version_tag_junction",
// KMS Service
KmsServerRootConfig = "kms_root_config",
KmsKey = "kms_keys",
KmsKeyVersion = "kms_key_versions"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

View File

@ -25,7 +25,8 @@ export const UsersSchema = z.object({
isEmailVerified: z.boolean().default(false).nullable().optional(),
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
isLocked: z.boolean().default(false).nullable().optional(),
temporaryLockDateEnd: z.date().nullable().optional()
temporaryLockDateEnd: z.date().nullable().optional(),
consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional()
});
export type TUsers = z.infer<typeof UsersSchema>;

View File

@ -9,6 +9,15 @@ export enum KeyStorePrefixes {
SecretReplication = "secret-replication-import-lock"
}
type TWaitTillReady = {
key: string;
waitingCb?: () => void;
keyCheckCb: (val: string | null) => boolean;
waitIteration?: number;
delay?: number;
jitter?: number;
};
export const keyStoreFactory = (redisUrl: string) => {
const redis = new Redis(redisUrl);
const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 });
@ -29,6 +38,29 @@ export const keyStoreFactory = (redisUrl: string) => {
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
const waitTillReady = async ({
key,
waitingCb,
keyCheckCb,
waitIteration = 10,
delay = 1000,
jitter = 200
}: TWaitTillReady) => {
let attempts = 0;
let isReady = keyCheckCb(await getItem(key));
while (!isReady) {
if (attempts > waitIteration) return;
// eslint-disable-next-line
await new Promise((resolve) => {
waitingCb?.();
setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter)));
});
attempts += 1;
// eslint-disable-next-line
isReady = keyCheckCb(await getItem(key, "wait_till_ready"));
}
};
return {
setItem,
getItem,
@ -37,6 +69,7 @@ export const keyStoreFactory = (redisUrl: string) => {
incrementBy,
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
return redisLock.acquire(resources, duration, settings);
}
},
waitTillReady
};
};

View File

@ -386,6 +386,8 @@ export const SECRET_IMPORTS = {
environment: "The slug of the environment to import into.",
path: "The path to import into.",
workspaceId: "The ID of the project you are working in.",
isReplication:
"When true, secrets from the source will be automatically sent to the destination. If approval policies exist at the destination, the secrets will be sent as approval requests instead of being applied immediately.",
import: {
environment: "The slug of the environment to import from.",
path: "The path to import from."
@ -661,6 +663,7 @@ export const INTEGRATION = {
targetServiceId:
"The service based grouping identifier ID of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank",
owner: "External integration providers service entity owner. Used in Github.",
url: "The self-hosted URL of the platform to integrate with",
path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault",
region: "AWS region to sync secrets to.",
scope: "Scope of the provider. Used by Github, Qovery",
@ -673,7 +676,8 @@ export const INTEGRATION = {
secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS secrets.",
kmsKeyId: "The ID of the encryption key from AWS KMS.",
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store."
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
shouldEnableDelete: "The flag to enable deletion of secrets"
}
},
UPDATE: {

View File

@ -75,6 +75,7 @@ const envSchema = z
.optional()
.default(process.env.URL_GITLAB_LOGIN ?? GITLAB_URL)
), // fallback since URL_GITLAB_LOGIN has been renamed
DEFAULT_SAML_ORG_SLUG: zpStr(z.string().optional()).default(process.env.NEXT_PUBLIC_SAML_ORG_SLUG),
// integration client secrets
// heroku
CLIENT_ID_HEROKU: zpStr(z.string().optional()),
@ -119,7 +120,8 @@ const envSchema = z
.transform((val) => val === "true")
.optional(),
INFISICAL_CLOUD: zodStrBool.default("false"),
MAINTENANCE_MODE: zodStrBool.default("false")
MAINTENANCE_MODE: zodStrBool.default("false"),
CAPTCHA_SECRET: zpStr(z.string().optional())
})
.transform((data) => ({
...data,
@ -131,7 +133,8 @@ const envSchema = z
isSecretScanningConfigured:
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET)
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG
}));
let envCfg: Readonly<z.infer<typeof envSchema>>;

View File

@ -0,0 +1,49 @@
import crypto from "crypto";
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
const getIvLength = () => {
return 12;
};
const getTagLength = () => {
return 16;
};
export const symmetricCipherService = (type: SymmetricEncryption): TSymmetricEncryptionFns => {
const IV_LENGTH = getIvLength();
const TAG_LENGTH = getTagLength();
const encrypt = (text: Buffer, key: Buffer) => {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(type, key, iv);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
// Get the authentication tag
const tag = cipher.getAuthTag();
// Concatenate IV, encrypted text, and tag into a single buffer
const ciphertextBlob = Buffer.concat([iv, encrypted, tag]);
return ciphertextBlob;
};
const decrypt = (ciphertextBlob: Buffer, key: Buffer) => {
// Extract the IV, encrypted text, and tag from the buffer
const iv = ciphertextBlob.subarray(0, IV_LENGTH);
const tag = ciphertextBlob.subarray(-TAG_LENGTH);
const encrypted = ciphertextBlob.subarray(IV_LENGTH, -TAG_LENGTH);
const decipher = crypto.createDecipheriv(type, key, iv);
decipher.setAuthTag(tag);
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
return decrypted;
};
return {
encrypt,
decrypt
};
};

View File

@ -0,0 +1,2 @@
export { symmetricCipherService } from "./cipher";
export { SymmetricEncryption } from "./types";

View File

@ -0,0 +1,9 @@
export enum SymmetricEncryption {
AES_GCM_256 = "aes-256-gcm",
AES_GCM_128 = "aes-128-gcm"
}
export type TSymmetricEncryptionFns = {
encrypt: (text: Buffer, key: Buffer) => Buffer;
decrypt: (blob: Buffer, key: Buffer) => Buffer;
};

View File

@ -11,6 +11,8 @@ import { getConfig } from "../config/env";
export const decodeBase64 = (s: string) => naclUtils.decodeBase64(s);
export const encodeBase64 = (u: Uint8Array) => naclUtils.encodeBase64(u);
export const randomSecureBytes = (length = 32) => crypto.randomBytes(length);
export type TDecryptSymmetricInput = {
ciphertext: string;
iv: string;

View File

@ -9,7 +9,8 @@ export {
encryptAsymmetric,
encryptSymmetric,
encryptSymmetric128BitHexKeyUTF8,
generateAsymmetricKeyPair
generateAsymmetricKeyPair,
randomSecureBytes
} from "./encryption";
export {
decryptIntegrationAuths,

View File

@ -7,3 +7,7 @@ export const zpStr = <T extends ZodTypeAny>(schema: T, opt: { stripNull: boolean
if (typeof val !== "string") return val;
return val.trim() || undefined;
}, schema);
export const zodBuffer = z.custom<Buffer>((data) => Buffer.isBuffer(data) || data instanceof Uint8Array, {
message: "Expected binary data (Buffer Or Uint8Array)"
});

View File

@ -28,7 +28,7 @@ export const readLimit: RateLimitOptions = {
// POST, PATCH, PUT, DELETE endpoints
export const writeLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: 50,
max: 200, // (too low, FA having issues so increasing it - maidul)
keyGenerator: (req) => req.realIp
};

View File

@ -97,6 +97,9 @@ import { integrationDALFactory } from "@app/services/integration/integration-dal
import { integrationServiceFactory } from "@app/services/integration/integration-service";
import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal";
import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
import { kmsDALFactory } from "@app/services/kms/kms-dal";
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
import { kmsServiceFactory } from "@app/services/kms/kms-service";
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
import { orgDALFactory } from "@app/services/org/org-dal";
@ -261,6 +264,9 @@ export const registerRoutes = async (
const dynamicSecretDAL = dynamicSecretDALFactory(db);
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
const kmsDAL = kmsDALFactory(db);
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
orgRoleDAL,
@ -269,6 +275,12 @@ export const registerRoutes = async (
projectDAL
});
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
const kmsService = kmsServiceFactory({
kmsRootConfigDAL,
keyStore,
kmsDAL
});
const trustedIpService = trustedIpServiceFactory({
licenseService,
projectDAL,
@ -823,6 +835,7 @@ export const registerRoutes = async (
await telemetryQueue.startTelemetryCheck();
await dailyResourceCleanUp.startCleanUp();
await kmsService.startService();
// inject all services
server.decorate<FastifyZodProvider["services"]>("services", {
@ -906,7 +919,8 @@ export const registerRoutes = async (
emailConfigured: z.boolean().optional(),
inviteOnlySignup: z.boolean().optional(),
redisConfigured: z.boolean().optional(),
secretScanningConfigured: z.boolean().optional()
secretScanningConfigured: z.boolean().optional(),
samlDefaultOrgSlug: z.string().optional()
})
}
},
@ -919,7 +933,8 @@ export const registerRoutes = async (
emailConfigured: cfg.isSmtpConfigured,
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
redisConfigured: cfg.isRedisConfigured,
secretScanningConfigured: cfg.isSecretScanningConfigured
secretScanningConfigured: cfg.isSecretScanningConfigured,
samlDefaultOrgSlug: cfg.samlDefaultOrgSlug
};
}
});

View File

@ -8,7 +8,7 @@ import { writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { IntegrationMappingBehavior } from "@app/services/integration-auth/integration-list";
import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
@ -42,39 +42,11 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetService: z.string().trim().optional().describe(INTEGRATION.CREATE.targetService),
targetServiceId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetServiceId),
owner: z.string().trim().optional().describe(INTEGRATION.CREATE.owner),
url: z.string().trim().optional().describe(INTEGRATION.CREATE.url),
path: z.string().trim().optional().describe(INTEGRATION.CREATE.path),
region: z.string().trim().optional().describe(INTEGRATION.CREATE.region),
scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope),
metadata: z
.object({
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
mappingBehavior: z
.nativeEnum(IntegrationMappingBehavior)
.optional()
.describe(INTEGRATION.CREATE.metadata.mappingBehavior),
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z
.object({
labelName: z.string(),
labelValue: z.string()
})
.optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
secretAWSTag: z
.array(
z.object({
key: z.string(),
value: z.string()
})
)
.optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete)
})
.default({})
metadata: IntegrationMetadataSchema.default({})
}),
response: {
200: z.object({
@ -160,33 +132,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment),
metadata: z
.object({
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
mappingBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.mappingBehavior),
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z
.object({
labelName: z.string(),
labelValue: z.string()
})
.optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
secretAWSTag: z
.array(
z.object({
key: z.string(),
value: z.string()
})
)
.optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete)
})
.optional()
metadata: IntegrationMetadataSchema.optional()
}),
response: {
200: z.object({

View File

@ -30,7 +30,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.import.environment),
path: z.string().trim().transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.import.path)
}),
isReplication: z.boolean().default(false)
isReplication: z.boolean().default(false).describe(SECRET_IMPORTS.CREATE.isReplication)
}),
response: {
200: z.object({

View File

@ -80,7 +80,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
body: z.object({
email: z.string().trim(),
providerAuthToken: z.string().trim().optional(),
clientProof: z.string().trim()
clientProof: z.string().trim(),
captchaToken: z.string().trim().optional()
}),
response: {
200: z.discriminatedUnion("mfaEnabled", [
@ -106,6 +107,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const data = await server.services.login.loginExchangeClientProof({
captchaToken: req.body.captchaToken,
email: req.body.email,
ip: req.realIp,
userAgent,

View File

@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@ -176,12 +177,16 @@ export const authLoginServiceFactory = ({
clientProof,
ip,
userAgent,
providerAuthToken
providerAuthToken,
captchaToken
}: TLoginClientProofDTO) => {
const appCfg = getConfig();
const userEnc = await userDAL.findUserEncKeyByUsername({
username: email
});
if (!userEnc) throw new Error("Failed to find user");
const user = await userDAL.findById(userEnc.userId);
const cfg = getConfig();
let authMethod = AuthMethod.EMAIL;
@ -196,6 +201,31 @@ export const authLoginServiceFactory = ({
}
}
if (
user.consecutiveFailedPasswordAttempts &&
user.consecutiveFailedPasswordAttempts >= 10 &&
Boolean(appCfg.CAPTCHA_SECRET)
) {
if (!captchaToken) {
throw new BadRequestError({
name: "Captcha Required",
message: "Accomplish the required captcha by logging in via Web"
});
}
// validate captcha token
const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
response: captchaToken,
secret: appCfg.CAPTCHA_SECRET
});
if (!response.data.success) {
throw new BadRequestError({
name: "Invalid Captcha"
});
}
}
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
const isValidClientProof = await srpCheckClientProof(
userEnc.salt,
@ -204,15 +234,31 @@ export const authLoginServiceFactory = ({
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
if (!isValidClientProof) {
await userDAL.update(
{ id: userEnc.userId },
{
$incr: {
consecutiveFailedPasswordAttempts: 1
}
}
);
throw new Error("Failed to authenticate. Try again?");
}
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null
});
await userDAL.updateById(userEnc.userId, {
consecutiveFailedPasswordAttempts: 0
});
// send multi factor auth token if they it enabled
if (userEnc.isMfaEnabled && userEnc.email) {
const user = await userDAL.findById(userEnc.userId);
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
const mfaToken = jwt.sign(

View File

@ -12,6 +12,7 @@ export type TLoginClientProofDTO = {
providerAuthToken?: string;
ip: string;
userAgent: string;
captchaToken?: string;
};
export type TVerifyMfaTokenDTO = {

View File

@ -199,6 +199,7 @@ export const integrationAuthServiceFactory = ({
projectId,
namespace,
integration,
url,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8,
...(integration === Integrations.GCP_SECRET_MANAGER

View File

@ -30,7 +30,8 @@ export enum Integrations {
DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform",
CLOUD_66 = "cloud-66",
NORTHFLANK = "northflank",
HASURA_CLOUD = "hasura-cloud"
HASURA_CLOUD = "hasura-cloud",
RUNDECK = "rundeck"
}
export enum IntegrationType {
@ -368,6 +369,15 @@ export const getIntegrationOptions = async () => {
type: "pat",
clientId: "",
docsLink: ""
},
{
name: "Rundeck",
slug: "rundeck",
image: "Rundeck.svg",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
}
];

View File

@ -27,9 +27,11 @@ import { z } from "zod";
import { SecretType, TIntegrationAuths, TIntegrations, TSecrets } from "@app/db/schemas";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-schema";
import {
IntegrationInitialSyncBehavior,
IntegrationMappingBehavior,
@ -521,18 +523,42 @@ const syncSecretsAWSParameterStore = async ({
.promise();
}
// case: secret exists in AWS parameter store
} else if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) {
// case: secret value doesn't match one in AWS parameter store
} else {
// -> update secret
await ssm
.putParameter({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key].value,
Overwrite: true
// Tags: metadata.secretAWSTag ? [{ Key: metadata.secretAWSTag.key, Value: metadata.secretAWSTag.value }] : []
})
.promise();
if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) {
await ssm
.putParameter({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key].value,
Overwrite: true
})
.promise();
}
if (awsParameterStoreSecretsObj[key].Name) {
try {
await ssm
.addTagsToResource({
ResourceType: "Parameter",
ResourceId: awsParameterStoreSecretsObj[key].Name as string,
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
Key: tag.key,
Value: tag.value
}))
: []
})
.promise();
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((err as any).code === "AccessDeniedException") {
logger.error(
`AWS Parameter Store Error [integration=${integration.id}]: double check AWS account permissions (refer to the Infisical docs)`
);
}
}
}
}
await new Promise((resolve) => {
@ -1338,38 +1364,41 @@ const syncSecretsGitHub = async ({
}
}
for await (const encryptedSecret of encryptedSecrets) {
if (
!(encryptedSecret.name in secrets) &&
!(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) &&
!(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix))
) {
switch (integration.scope) {
case GithubScope.Org: {
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: encryptedSecret.name
});
break;
}
case GithubScope.Env: {
await octokit.request(
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string,
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
if (metadata.shouldEnableDelete) {
for await (const encryptedSecret of encryptedSecrets) {
if (
!(encryptedSecret.name in secrets) &&
!(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) &&
!(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix))
) {
switch (integration.scope) {
case GithubScope.Org: {
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: encryptedSecret.name
}
);
break;
}
default: {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: encryptedSecret.name
});
break;
});
break;
}
case GithubScope.Env: {
await octokit.request(
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string,
secret_name: encryptedSecret.name
}
);
break;
}
default: {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: encryptedSecret.name
});
break;
}
}
}
}
@ -2725,6 +2754,20 @@ const syncSecretsCloudflarePages = async ({
}
}
);
const metadata = z.record(z.any()).parse(integration.metadata);
if (metadata.shouldAutoRedeploy) {
await request.post(
`${IntegrationUrls.CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}/deployments`,
{},
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
}
);
}
};
/**
@ -3330,6 +3373,82 @@ const syncSecretsHasuraCloud = async ({
}
};
/** Sync/push [secrets] to Rundeck
* @param {Object} obj
* @param {TIntegrations} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Rundeck integration
*/
const syncSecretsRundeck = async ({
integration,
secrets,
accessToken
}: {
integration: TIntegrations;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
interface RundeckSecretResource {
name: string;
}
interface RundeckSecretsGetRes {
resources: RundeckSecretResource[];
}
let existingRundeckSecrets: string[] = [];
try {
const listResult = await request.get<RundeckSecretsGetRes>(
`${integration.url}/api/44/storage/${integration.path}`,
{
headers: {
"X-Rundeck-Auth-Token": accessToken
}
}
);
existingRundeckSecrets = listResult.data.resources.map((res) => res.name);
} catch (err) {
logger.info("No existing rundeck secrets");
}
try {
for await (const [key, value] of Object.entries(secrets)) {
if (existingRundeckSecrets.includes(key)) {
await request.put(`${integration.url}/api/44/storage/${integration.path}/${key}`, value.value, {
headers: {
"X-Rundeck-Auth-Token": accessToken,
"Content-Type": "application/x-rundeck-data-password"
}
});
} else {
await request.post(`${integration.url}/api/44/storage/${integration.path}/${key}`, value.value, {
headers: {
"X-Rundeck-Auth-Token": accessToken,
"Content-Type": "application/x-rundeck-data-password"
}
});
}
}
for await (const existingSecret of existingRundeckSecrets) {
if (!(existingSecret in secrets)) {
await request.delete(`${integration.url}/api/44/storage/${integration.path}/${existingSecret}`, {
headers: {
"X-Rundeck-Auth-Token": accessToken
}
});
}
}
} catch (err: unknown) {
throw new Error(
`Ensure that the provided Rundeck URL is accessible by Infisical and that the linked API token has sufficient permissions.\n\n${
(err as Error).message
}`
);
}
};
/**
* Sync/push [secrets] to [app] in integration named [integration]
*
@ -3596,6 +3715,13 @@ export const syncIntegrationSecrets = async ({
accessToken
});
break;
case Integrations.RUNDECK:
await syncSecretsRundeck({
integration,
secrets,
accessToken
});
break;
default:
throw new BadRequestError({ message: "Invalid integration" });
}

View File

@ -0,0 +1,35 @@
import { z } from "zod";
import { INTEGRATION } from "@app/lib/api-docs";
import { IntegrationMappingBehavior } from "../integration-auth/integration-list";
export const IntegrationMetadataSchema = z.object({
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
mappingBehavior: z
.nativeEnum(IntegrationMappingBehavior)
.optional()
.describe(INTEGRATION.CREATE.metadata.mappingBehavior),
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z
.object({
labelName: z.string(),
labelValue: z.string()
})
.optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
secretAWSTag: z
.array(
z.object({
key: z.string(),
value: z.string()
})
)
.optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete),
shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete)
});

View File

@ -43,6 +43,7 @@ export const integrationServiceFactory = ({
scope,
actorId,
region,
url,
isActive,
metadata,
secretPath,
@ -87,6 +88,7 @@ export const integrationServiceFactory = ({
region,
scope,
owner,
url,
appId,
path,
app,

View File

@ -12,6 +12,7 @@ export type TCreateIntegrationDTO = {
targetService?: string;
targetServiceId?: string;
owner?: string;
url?: string;
path?: string;
region?: string;
scope?: string;
@ -28,6 +29,7 @@ export type TCreateIntegrationDTO = {
}[];
kmsKeyId?: string;
shouldDisableDelete?: boolean;
shouldEnableDelete?: boolean;
};
} & Omit<TProjectPermission, "projectId">;
@ -53,6 +55,7 @@ export type TUpdateIntegrationDTO = {
}[];
kmsKeyId?: string;
shouldDisableDelete?: boolean;
shouldEnableDelete?: boolean;
};
} & Omit<TProjectPermission, "projectId">;

View File

@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TKmsDALFactory = ReturnType<typeof kmsDALFactory>;
export const kmsDALFactory = (db: TDbClient) => {
const kmsOrm = ormify(db, TableName.KmsKey);
return kmsOrm;
};

View File

@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TKmsRootConfigDALFactory = ReturnType<typeof kmsRootConfigDALFactory>;
export const kmsRootConfigDALFactory = (db: TDbClient) => {
const kmsOrm = ormify(db, TableName.KmsServerRootConfig);
return kmsOrm;
};

View File

@ -0,0 +1,126 @@
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { randomSecureBytes } from "@app/lib/crypto";
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TKmsDALFactory } from "./kms-dal";
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
import { TDecryptWithKmsDTO, TEncryptWithKmsDTO, TGenerateKMSDTO } from "./kms-types";
type TKmsServiceFactoryDep = {
kmsDAL: TKmsDALFactory;
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
};
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
const KMS_ROOT_CREATION_WAIT_TIME = 10;
// akhilmhdh: Don't edit this value. This is measured for blob concatination in kms
const KMS_VERSION = "v01";
const KMS_VERSION_BLOB_LENGTH = 3;
export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsServiceFactoryDep) => {
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
// this is used symmetric encryption
const generateKmsKey = async ({ scopeId, scopeType, isReserved = true }: TGenerateKMSDTO) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKeyMaterial = randomSecureBytes(32);
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
const { encryptedKey, ...doc } = await kmsDAL.create({
version: 1,
encryptedKey: encryptedKeyMaterial,
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
isReserved,
orgId: scopeType === "org" ? scopeId : undefined,
projectId: scopeType === "project" ? scopeId : undefined
});
return doc;
};
const encrypt = async ({ kmsId, plainText }: TEncryptWithKmsDTO) => {
const kmsDoc = await kmsDAL.findById(kmsId);
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" });
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY);
const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey);
// Buffer#1 encrypted text + Buffer#2 version number
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
return { cipherTextBlob };
};
const decrypt = async ({ cipherTextBlob: versionedCipherTextBlob, kmsId }: TDecryptWithKmsDTO) => {
const kmsDoc = await kmsDAL.findById(kmsId);
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" });
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY);
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey);
return decryptedBlob;
};
const startService = async () => {
const appCfg = getConfig();
// This will switch to a seal process and HMS flow in future
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
// if root key its base64 encoded
const isBase64 = !appCfg.ENCRYPTION_KEY;
if (!encryptionKey) throw new Error("Root encryption key not found for KMS service.");
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null);
if (!lock) {
await keyStore.waitTillReady({
key: KMS_ROOT_CREATION_WAIT_KEY,
keyCheckCb: (val) => val === "true",
waitingCb: () => logger.info("KMS. Waiting for leader to finish creation of KMS Root Key")
});
}
// check if KMS root key was already generated and saved in DB
const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
if (kmsRootConfig) {
if (lock) await lock.release();
logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting.");
const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
// set the flag so that other instancen nodes can start
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
logger.info("KMS: Loading ROOT Key into Memory.");
ROOT_ENCRYPTION_KEY = decryptedRootKey;
return;
}
logger.info("KMS: Generating ROOT Key");
const newRootKey = randomSecureBytes(32);
const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer);
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID });
// set the flag so that other instancen nodes can start
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
logger.info("KMS: Saved and loaded ROOT Key into memory");
if (lock) await lock.release();
ROOT_ENCRYPTION_KEY = newRootKey;
};
return {
startService,
generateKmsKey,
encrypt,
decrypt
};
};

View File

@ -0,0 +1,15 @@
export type TGenerateKMSDTO = {
scopeType: "project" | "org";
scopeId: string;
isReserved?: boolean;
};
export type TEncryptWithKmsDTO = {
kmsId: string;
plainText: Buffer;
};
export type TDecryptWithKmsDTO = {
kmsId: string;
cipherTextBlob: Buffer;
};

View File

@ -309,7 +309,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
};
const expandSecrets = async (
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => {
const expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {};
@ -329,8 +329,8 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
// should not do multi line encoding if user has set it to skip
// eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding
? expandedSec[key]
: formatMultiValueEnv(expandedSec[key]);
? formatMultiValueEnv(expandedSec[key])
: expandedSec[key];
// eslint-disable-next-line
continue;
}
@ -347,7 +347,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
);
// eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding ? expandedVal : formatMultiValueEnv(expandedVal);
secrets[key].value = secrets[key].skipMultilineEncoding ? formatMultiValueEnv(expandedVal) : expandedVal;
}
return secrets;
@ -395,7 +395,8 @@ export const decryptSecretRaw = (
type: secret.type,
_id: secret.id,
id: secret.id,
user: secret.userId
user: secret.userId,
skipMultilineEncoding: secret.skipMultilineEncoding
};
};

View File

@ -67,7 +67,10 @@ const MAX_SYNC_SECRET_DEPTH = 5;
export const uniqueSecretQueueKey = (environment: string, secretPath: string) =>
`secret-queue-dedupe-${environment}-${secretPath}`;
type TIntegrationSecret = Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>;
type TIntegrationSecret = Record<
string,
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined }
>;
export const secretQueueFactory = ({
queueService,
integrationDAL,

View File

@ -971,10 +971,24 @@ export const secretServiceFactory = ({
});
const batchSecretsExpand = async (
secretBatch: { secretKey: string; secretValue: string; secretComment?: string; secretPath: string }[]
secretBatch: {
secretKey: string;
secretValue: string;
secretComment?: string;
secretPath: string;
skipMultilineEncoding: boolean | null | undefined;
}[]
) => {
// Group secrets by secretPath
const secretsByPath: Record<string, { secretKey: string; secretValue: string; secretComment?: string }[]> = {};
const secretsByPath: Record<
string,
{
secretKey: string;
secretValue: string;
secretComment?: string;
skipMultilineEncoding: boolean | null | undefined;
}[]
> = {};
secretBatch.forEach((secret) => {
if (!secretsByPath[secret.secretPath]) {
@ -990,11 +1004,15 @@ export const secretServiceFactory = ({
continue;
}
const secretRecord: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }> = {};
const secretRecord: Record<
string,
{ value: string; comment?: string; skipMultilineEncoding: boolean | null | undefined }
> = {};
secretsByPath[secPath].forEach((decryptedSecret) => {
secretRecord[decryptedSecret.secretKey] = {
value: decryptedSecret.secretValue,
comment: decryptedSecret.secretComment
comment: decryptedSecret.secretComment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
};
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 KiB

View File

@ -0,0 +1,39 @@
---
title: "Rundeck"
description: "How to sync secrets from Infisical to Rundeck"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
<Steps>
<Step title="Authorize Infisical for Rundeck">
Obtain a User API Token in the Profile settings of Rundeck
![integrations rundeck token](../../images/integrations/rundeck/integrations-rundeck-token.png)
Navigate to your project's integrations tab in Infisical.
![integrations](../../images/integrations.png)
Press on the Rundeck tile and input your Rundeck instance Base URL and User API token to grant Infisical access to manage Rundeck keys
![integrations rundeck authorization](../../images/integrations/rundeck/integrations-rundeck-auth.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant
Infisical access to your project's environment variables. Although this step
breaks E2EE, it's necessary for Infisical to sync the environment variables to
the cloud platform.
</Info>
</Step>
<Step title="Start integration">
Select which Infisical environment secrets you want to sync to a Rundeck Key Storage Path and press create integration to start syncing secrets to Rundeck.
![create integration rundeck](../../images/integrations/rundeck/integrations-rundeck-create.png)
![integrations rundeck](../../images/integrations/rundeck/integrations-rundeck.png)
</Step>
</Steps>

View File

@ -28,6 +28,7 @@ Prerequisites:
"Action": [
"ssm:PutParameter",
"ssm:DeleteParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath",
"ssm:DeleteParameters",
"ssm:AddTagsToResource", // if you need to add tags to secrets

View File

@ -26,14 +26,14 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
| [Supabase](/integrations/cloud/supabase) | Cloud | Available |
| [Northflank](/integrations/cloud/northflank) | Cloud | Available |
| [Cloudflare Pages](/integrations/cloud/cloudflare-pages) | Cloud | Available |
| [Cloudflare Workers](/integrations/cloud/cloudflare-workers) | Cloud | Available |
| [Cloudflare Workers](/integrations/cloud/cloudflare-workers) | Cloud | Available |
| [Checkly](/integrations/cloud/checkly) | Cloud | Available |
| [Qovery](/integrations/cloud/qovery) | Cloud | Available |
| [Qovery](/integrations/cloud/qovery) | Cloud | Available |
| [HashiCorp Vault](/integrations/cloud/hashicorp-vault) | Cloud | Available |
| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available |
| [AWS Secrets Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |
| [AWS Secrets Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available |
| [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available |
| [GCP Secret Manager](/integrations/cloud/gcp-secret-manager) | Cloud | Available |
| [GCP Secret Manager](/integrations/cloud/gcp-secret-manager) | Cloud | Available |
| [Windmill](/integrations/cloud/windmill) | Cloud | Available |
| [BitBucket](/integrations/cicd/bitbucket) | CI/CD | Available |
| [Codefresh](/integrations/cicd/codefresh) | CI/CD | Available |
@ -41,6 +41,7 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
| [GitLab](/integrations/cicd/gitlab) | CI/CD | Available |
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available |
| [Travis CI](/integrations/cicd/travisci) | CI/CD | Available |
| [Rundeck](/integrations/cicd/rundeck) | CI/CD | Available |
| [React](/integrations/frameworks/react) | Framework | Available |
| [Vue](/integrations/frameworks/vue) | Framework | Available |
| [Express](/integrations/frameworks/express) | Framework | Available |

View File

@ -496,7 +496,6 @@ To enable auto redeployment you simply have to add the following annotation to t
```yaml
secrets.infisical.com/auto-reload: "true"
```
<Accordion title="Deployment example with auto redeploy enabled">
```yaml
apiVersion: apps/v1
@ -527,7 +526,11 @@ spec:
- containerPort: 80
```
</Accordion>
<Info>
#### How it works
When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update.
Then, for each deployment that has this annotation present, a rolling update will be triggered.
</Info>
## Global configuration
To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap.

View File

@ -32,10 +32,7 @@
"thumbsRating": true
},
"api": {
"baseUrl": [
"https://app.infisical.com",
"http://localhost:8080"
]
"baseUrl": ["https://app.infisical.com", "http://localhost:8080"]
},
"topbarLinks": [
{
@ -76,9 +73,7 @@
"documentation/getting-started/introduction",
{
"group": "Quickstart",
"pages": [
"documentation/guides/local-development"
]
"pages": ["documentation/guides/local-development"]
},
{
"group": "Guides",
@ -221,9 +216,7 @@
},
{
"group": "Reference architectures",
"pages": [
"self-hosting/reference-architectures/aws-ecs"
]
"pages": ["self-hosting/reference-architectures/aws-ecs"]
},
"self-hosting/ee",
"self-hosting/faq"
@ -343,6 +336,7 @@
"pages": [
"integrations/cicd/circleci",
"integrations/cicd/travisci",
"integrations/cicd/rundeck",
"integrations/cicd/codefresh",
"integrations/cloud/checkly"
]
@ -379,15 +373,11 @@
},
{
"group": "Build Tool Integrations",
"pages": [
"integrations/build-tools/gradle"
]
"pages": ["integrations/build-tools/gradle"]
},
{
"group": "",
"pages": [
"sdks/overview"
]
"pages": ["sdks/overview"]
},
{
"group": "SDK's",
@ -405,9 +395,7 @@
"api-reference/overview/authentication",
{
"group": "Examples",
"pages": [
"api-reference/overview/examples/integration"
]
"pages": ["api-reference/overview/examples/integration"]
}
]
},
@ -563,15 +551,11 @@
},
{
"group": "Service Tokens",
"pages": [
"api-reference/endpoints/service-tokens/get"
]
"pages": ["api-reference/endpoints/service-tokens/get"]
},
{
"group": "Audit Logs",
"pages": [
"api-reference/endpoints/audit-logs/export-audit-log"
]
"pages": ["api-reference/endpoints/audit-logs/export-audit-log"]
}
]
},
@ -587,9 +571,7 @@
},
{
"group": "",
"pages": [
"changelog/overview"
]
"pages": ["changelog/overview"]
},
{
"group": "Contributing",
@ -613,9 +595,7 @@
},
{
"group": "Contributing to SDK",
"pages": [
"contributing/sdk/developing"
]
"pages": ["contributing/sdk/developing"]
}
]
}

View File

@ -318,6 +318,11 @@ SMTP_FROM_NAME=Infisical
By default, users can only login via email/password based login method.
To login into Infisical with OAuth providers such as Google, configure the associated variables.
<ParamField query="DEFAULT_SAML_ORG_SLUG" type="string">
When set, all visits to the Infisical login page will automatically redirect users of your Infisical instance to the SAML identity provider associated with the specified organization slug.
</ParamField>
<Accordion title="Google">
Follow detailed guide to configure [Google SSO](/documentation/platform/sso/google)
@ -369,11 +374,6 @@ To login into Infisical with OAuth providers such as Google, configure the assoc
information.
</Accordion>
<ParamField query="NEXT_PUBLIC_SAML_ORG_SLUG" type="string">
Configure SAML organization slug to automatically redirect all users of your
Infisical instance to the identity provider.
</ParamField>
## Native secret integrations
To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box.

View File

@ -33,7 +33,7 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete
pullPolicy: IfNotPresent
```
<Warning>
Do you not use the latest docker image tag in production deployments as they can introduce unexpected changes
Do not use the latest docker image tag in production deployments as they can introduce unexpected changes
</Warning>
</Step>

View File

@ -2,6 +2,7 @@ ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id
ARG NEXT_INFISICAL_PLATFORM_VERSION=next-infisical-platform-version
ARG CAPTCHA_SITE_KEY=captcha-site-key
FROM node:16-alpine AS deps
# Install dependencies only when needed. Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
@ -31,6 +32,8 @@ ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
# Build
RUN npm run build
@ -57,7 +60,9 @@ ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \
BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
ARG NEXT_INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION=$NEXT_INFISICAL_PLATFORM_VERSION
ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
COPY --chown=nextjs:nodejs --chmod=555 scripts ./scripts
COPY --from=builder /app/public ./public
RUN chown nextjs:nodejs ./public/data

View File

@ -1,13 +1,12 @@
const path = require("path");
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self' https://app.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com 'unsafe-inline' 'unsafe-eval';
style-src 'self' https://rsms.me 'unsafe-inline';
script-src 'self' https://app.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
child-src https://api.stripe.com;
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/;
connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://127.0.0.1:*;
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/ https://hcaptcha.com https://*.hcaptcha.com;
connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://127.0.0.1:* https://hcaptcha.com https://*.hcaptcha.com;
img-src 'self' https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:;
media-src https://js.intercomcdn.com;
font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com;

View File

@ -4,7 +4,6 @@
"requires": true,
"packages": {
"": {
"name": "frontend",
"dependencies": {
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",
@ -19,6 +18,7 @@
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@hcaptcha/react-hcaptcha": "^1.10.1",
"@headlessui/react": "^1.7.7",
"@hookform/resolvers": "^2.9.10",
"@octokit/rest": "^19.0.7",
@ -3200,6 +3200,24 @@
"react": ">=16.3"
}
},
"node_modules/@hcaptcha/loader": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@hcaptcha/loader/-/loader-1.2.4.tgz",
"integrity": "sha512-3MNrIy/nWBfyVVvMPBKdKrX7BeadgiimW0AL/a/8TohNtJqxoySKgTJEXOQvYwlHemQpUzFrIsK74ody7JiMYw=="
},
"node_modules/@hcaptcha/react-hcaptcha": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.10.1.tgz",
"integrity": "sha512-P0en4gEZAecah7Pt3WIaJO2gFlaLZKkI0+Tfdg8fNqsDxqT9VytZWSkH4WAkiPRULK1QcGgUZK+J56MXYmPifw==",
"dependencies": {
"@babel/runtime": "^7.17.9",
"@hcaptcha/loader": "^1.2.1"
},
"peerDependencies": {
"react": ">= 16.3.0",
"react-dom": ">= 16.3.0"
}
},
"node_modules/@headlessui/react": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz",

View File

@ -26,6 +26,7 @@
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@hcaptcha/react-hcaptcha": "^1.10.1",
"@headlessui/react": "^1.7.7",
"@hookform/resolvers": "^2.9.10",
"@octokit/rest": "^19.0.7",

View File

@ -32,7 +32,8 @@ const integrationSlugNameMapping: Mapping = {
northflank: "Northflank",
windmill: "Windmill",
"gcp-secret-manager": "GCP Secret Manager",
"hasura-cloud": "Hasura Cloud"
"hasura-cloud": "Hasura Cloud",
rundeck: "Rundeck"
};
const envMapping: Mapping = {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="45.359 114.637 60.122 58.576"><path d="M46.83 113.864l7.608 12.01H92.5l-7.543-12.01zm15.26 23.98l3.684 5.754-3.968 6.32h38.4l3.815-6.017-3.815-5.907h-38.04zm-7.826 24.13l-7.455 11.77v.24h38.148l7.564-12.012z" fill="#f91629"/></svg>

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 KiB

After

Width:  |  Height:  |  Size: 212 KiB

View File

@ -4,7 +4,7 @@ scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID"
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG"
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY" "$NEXT_PUBLIC_CAPTCHA_SITE_KEY"
if [ "$TELEMETRY_ENABLED" != "false" ]; then
echo "Telemetry is enabled"

View File

@ -6,6 +6,8 @@ scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTER
scripts/replace-variable.sh "$BAKED_NEXT_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG"
scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY" "$NEXT_PUBLIC_CAPTCHA_SITE_KEY"
if [ "$TELEMETRY_ENABLED" != "false" ]; then
echo "Telemetry is enabled"
scripts/set-telemetry.sh true

View File

@ -30,11 +30,13 @@ export interface IsCliLoginSuccessful {
const attemptLogin = async ({
email,
password,
providerAuthToken
providerAuthToken,
captchaToken
}: {
email: string;
password: string;
providerAuthToken?: string;
captchaToken?: string;
}): Promise<IsCliLoginSuccessful> => {
const telemetry = new Telemetry().getInstance();
return new Promise((resolve, reject) => {
@ -70,7 +72,8 @@ const attemptLogin = async ({
} = await login2({
email,
clientProof,
providerAuthToken
providerAuthToken,
captchaToken
});
if (mfaEnabled) {
// case: MFA is enabled

View File

@ -22,11 +22,13 @@ interface IsLoginSuccessful {
const attemptLogin = async ({
email,
password,
providerAuthToken
providerAuthToken,
captchaToken
}: {
email: string;
password: string;
providerAuthToken?: string;
captchaToken?: string;
}): Promise<IsLoginSuccessful> => {
const telemetry = new Telemetry().getInstance();
// eslint-disable-next-line new-cap
@ -58,6 +60,7 @@ const attemptLogin = async ({
iv,
tag
} = await login2({
captchaToken,
email,
clientProof,
providerAuthToken

View File

@ -2,5 +2,6 @@ const ENV = process.env.NEXT_PUBLIC_ENV! || "development"; // investigate
const POSTHOG_API_KEY = process.env.NEXT_PUBLIC_POSTHOG_API_KEY!;
const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
const INTERCOMid = process.env.NEXT_PUBLIC_INTERCOMid!;
const CAPTCHA_SITE_KEY = process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY!;
export { ENV, INTERCOMid, POSTHOG_API_KEY, POSTHOG_HOST };
export { CAPTCHA_SITE_KEY, ENV, INTERCOMid, POSTHOG_API_KEY, POSTHOG_HOST };

View File

@ -62,6 +62,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
<SelectPrimitive.Content
className={twMerge(
"relative top-1 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md",
position === "popper" && "max-h-72",
dropdownContainerClassName
)}
position={position}
@ -113,7 +114,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
isSelected && "bg-primary",
isDisabled &&
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
className
)}
ref={forwardedRef}

View File

@ -0,0 +1,5 @@
export const removeTrailingSlash = (str: string) => {
if (str === "/") return str;
return str.endsWith("/") ? str.slice(0, -1) : str;
};

View File

@ -30,6 +30,7 @@ export type Login1DTO = {
};
export type Login2DTO = {
captchaToken?: string;
email: string;
clientProof: string;
providerAuthToken?: string;

View File

@ -7,6 +7,7 @@ export type IntegrationAuth = {
updatedAt: string;
algorithm: string;
keyEncoding: string;
url?: string;
teamId?: string;
};

View File

@ -41,6 +41,7 @@ export const useCreateIntegration = () => {
owner,
path,
region,
url,
scope,
secretPath,
metadata
@ -56,6 +57,7 @@ export const useCreateIntegration = () => {
targetService?: string;
targetServiceId?: string;
owner?: string;
url?: string;
path?: string;
region?: string;
scope?: string;
@ -71,6 +73,7 @@ export const useCreateIntegration = () => {
}[];
kmsKeyId?: string;
shouldDisableDelete?: boolean;
shouldEnableDelete?: boolean;
};
}) => {
const {
@ -85,6 +88,7 @@ export const useCreateIntegration = () => {
targetEnvironmentId,
targetService,
targetServiceId,
url,
owner,
path,
scope,

View File

@ -4,4 +4,5 @@ export type ServerStatus = {
emailConfigured: boolean;
secretScanningConfigured: boolean;
redisConfigured: boolean;
samlDefaultOrgSlug: boolean
};

View File

@ -7,7 +7,15 @@ import { createNotification } from "@app/components/notifications";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api";
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2";
import {
Button,
Card,
CardTitle,
FormControl,
Select,
SelectItem,
Switch
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
@ -34,6 +42,7 @@ export default function CloudflarePagesIntegrationPage() {
const [targetApp, setTargetApp] = useState("");
const [targetAppId, setTargetAppId] = useState("");
const [targetEnvironment, setTargetEnvironment] = useState("");
const [shouldAutoRedeploy, setShouldAutoRedeploy] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@ -69,7 +78,10 @@ export default function CloudflarePagesIntegrationPage() {
appId: targetAppId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
secretPath
secretPath,
metadata: {
shouldAutoRedeploy
}
});
setIsLoading(false);
@ -169,6 +181,15 @@ export default function CloudflarePagesIntegrationPage() {
))}
</Select>
</FormControl>
<div className="mb-[2.36rem] ml-1 px-6">
<Switch
id="redeploy-cloudflare-pages"
onCheckedChange={(isChecked: boolean) => setShouldAutoRedeploy(isChecked)}
isChecked={shouldAutoRedeploy}
>
Auto-redeploy service upon secret change
</Switch>
</div>
<Button
onClick={handleButtonClick}
color="mineshaft"

View File

@ -33,6 +33,7 @@ import {
Input,
Select,
SelectItem,
Switch,
Tab,
TabList,
TabPanel,
@ -59,7 +60,7 @@ const schema = yup.object({
selectedSourceEnvironment: yup.string().trim().required("Project Environment is required"),
secretPath: yup.string().trim().required("Secrets Path is required"),
secretSuffix: yup.string().trim().optional(),
shouldEnableDelete: yup.boolean().optional(),
scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(),
repoIds: yup.mixed().when("scope", {
@ -98,7 +99,6 @@ type FormData = yup.InferType<typeof schema>;
export default function GitHubCreateIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const integrationAuthId =
(queryString.parse(router.asPath.split("?")[1]).integrationAuthId as string) ?? "";
@ -120,7 +120,8 @@ export default function GitHubCreateIntegrationPage() {
defaultValues: {
secretPath: "/",
scope: "github-repo",
repoIds: []
repoIds: [],
shouldEnableDelete: false
}
});
@ -177,7 +178,8 @@ export default function GitHubCreateIntegrationPage() {
app: targetApp.name, // repo name
owner: targetApp.owner, // repo owner
metadata: {
secretSuffix: data.secretSuffix
secretSuffix: data.secretSuffix,
shouldEnableDelete: data.shouldEnableDelete
}
});
})
@ -194,7 +196,8 @@ export default function GitHubCreateIntegrationPage() {
scope: data.scope,
owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name,
metadata: {
secretSuffix: data.secretSuffix
secretSuffix: data.secretSuffix,
shouldEnableDelete: data.shouldEnableDelete
}
});
break;
@ -211,7 +214,8 @@ export default function GitHubCreateIntegrationPage() {
owner: repoOwner,
targetEnvironmentId: data.envId,
metadata: {
secretSuffix: data.secretSuffix
secretSuffix: data.secretSuffix,
shouldEnableDelete: data.shouldEnableDelete
}
});
break;
@ -546,6 +550,21 @@ export default function GitHubCreateIntegrationPage() {
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="ml-1 mb-5">
<Controller
control={control}
name="shouldEnableDelete"
render={({ field: { onChange, value } }) => (
<Switch
id="delete-github-option"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
>
Delete secrets in Github that are not in Infisical
</Switch>
)}
/>
</div>
<Controller
control={control}
name="secretSuffix"

View File

@ -0,0 +1,129 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
const schema = z.object({
authToken: z.string().trim().min(1, { message: "Rundeck Auth Token is required" }),
rundeckURL: z.string().trim().min(1, {
message: "Rundeck URL is required"
})
});
type FormData = z.infer<typeof schema>;
export default function RundeckAuthorizeIntegrationPage() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const { mutateAsync } = useSaveIntegrationAccessToken();
const { control, handleSubmit } = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
authToken: "",
rundeckURL: ""
}
});
const onFormSubmit = async ({ authToken, rundeckURL }: FormData) => {
try {
setIsLoading(true);
const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"),
integration: "rundeck",
accessToken: authToken,
url: rundeckURL.trim()
});
setIsLoading(false);
router.push(`/integrations/rundeck/create?integrationAuthId=${integrationAuth.id}`);
} catch (err) {
setIsLoading(false);
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Head>
<title>Authorize Rundeck Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
<CardTitle
className="px-6 text-left text-xl"
subTitle="After adding your URL and auth token, you will be prompted to set up an integration for a particular Infisical project and environment."
>
<div className="flex flex-row items-center">
<div className="flex items-center pb-0.5">
<Image
src="/images/integrations/Rundeck.svg"
height={30}
width={30}
alt="Rundeck logo"
/>
</div>
<span className="ml-2.5">Rundeck Integration </span>
<Link href="https://infisical.com/docs/integrations/cicd/rundeck" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
</CardTitle>
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6 pb-8 text-right">
<Controller
control={control}
name="rundeckURL"
render={({ field, fieldState: { error } }) => (
<FormControl label="URL" errorText={error?.message} isError={Boolean(error)}>
<Input {...field} placeholder="https://self-hosted-rundeck.com" />
</FormControl>
)}
/>
<Controller
control={control}
name="authToken"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Rundeck Auth Token"
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="" />
</FormControl>
)}
/>
<Button
colorSchema="primary"
variant="outline_bg"
className="mt-2 w-min"
size="sm"
type="submit"
isLoading={isLoading}
>
Connect to Rundeck
</Button>
</form>
</Card>
</div>
);
}
RundeckAuthorizeIntegrationPage.requireAuth = true;

View File

@ -0,0 +1,217 @@
import { Controller, useForm } from "react-hook-form";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import queryString from "query-string";
import { z } from "zod";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { useCreateIntegration } from "@app/hooks/api";
import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth";
import { useGetWorkspaceById } from "@app/hooks/api/workspace";
const schema = z.object({
keyStoragePath: z.string().trim().min(1, { message: "Rundeck Key Storage path is required" }),
secretPath: z.string().trim().min(1, { message: "Secret path is required" }),
sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" })
});
type TFormSchema = z.infer<typeof schema>;
export default function RundeckCreateIntegrationPage() {
const {
control,
handleSubmit,
watch,
formState: { isSubmitting }
} = useForm<TFormSchema>({
resolver: zodResolver(schema),
defaultValues: {
secretPath: "/"
}
});
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth, isLoading: isIntegrationAuthLoading } = useGetIntegrationAuthById(
(integrationAuthId as string) ?? ""
);
const selectedSourceEnvironment = watch("sourceEnvironment");
const onFormSubmit = async ({ secretPath, sourceEnvironment, keyStoragePath }: TFormSchema) => {
try {
if (!integrationAuth?.id) return;
await mutateAsync({
integrationAuthId: integrationAuth?.id,
isActive: true,
path: keyStoragePath,
sourceEnvironment,
url: integrationAuth.url,
secretPath
});
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth && workspace ? (
<div className="flex h-full w-full flex-col items-center justify-center">
<Head>
<title>Set Up Rundeck Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<Card className="max-w-lg rounded-md border border-mineshaft-600">
<CardTitle
className="px-6 text-left text-xl"
subTitle="Choose which environment or folder in Infisical you want to sync to the Rundeck Key Storage."
>
<div className="flex flex-row items-center">
<div className="flex items-center pb-0.5">
<Image
src="/images/integrations/Rundeck.svg"
height={30}
width={30}
alt="Rundeck logo"
/>
</div>
<span className="ml-2.5">Rundeck Integration </span>
<Link href="https://infisical.com/docs/integrations/cloud/flyio" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
</CardTitle>
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col px-6">
<Controller
control={control}
name="sourceEnvironment"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Environment"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
className="w-full border border-mineshaft-500"
value={field.value}
onValueChange={(val) => {
field.onChange(val);
}}
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
<SecretPathInput {...field} environment={selectedSourceEnvironment} />
</FormControl>
)}
/>
<Controller
control={control}
name="keyStoragePath"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Rundeck Key Storage Path"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
placeholder={`keys/project/${workspace.name
.toLowerCase()
.replace(/ /g, "-")}/${selectedSourceEnvironment}`}
{...field}
/>
</FormControl>
)}
/>
<Button
type="submit"
color="mineshaft"
variant="outline_bg"
className="mb-6 mt-2 ml-auto"
isLoading={isSubmitting}
>
Create Integration
</Button>
</form>
</Card>
</div>
) : (
<div className="flex h-full w-full items-center justify-center">
<Head>
<title>Set Up Rundeck Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
{isIntegrationAuthLoading ? (
<img
src="/images/loading/loading.gif"
height={70}
width={120}
alt="infisical loading indicator"
/>
) : (
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
<p>
Something went wrong. Please contact{" "}
<a
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
target="_blank"
rel="noopener noreferrer"
href="mailto:support@infisical.com"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
}
RundeckCreateIntegrationPage.requireAuth = true;

View File

@ -128,6 +128,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "hasura-cloud":
link = `${window.location.origin}/integrations/hasura-cloud/authorize`;
break;
case "rundeck":
link = `${window.location.origin}/integrations/rundeck/authorize`;
break;
default:
break;
}

View File

@ -141,7 +141,8 @@ export const IntegrationsSection = ({
label={
(integration.integration === "qovery" && integration?.scope) ||
(integration.integration === "aws-secret-manager" && "Secret") ||
(integration.integration === "aws-parameter-store" && "Path") ||
(["aws-parameter-store", "rundeck"].includes(integration.integration) &&
"Path") ||
(integration?.integration === "terraform-cloud" && "Project") ||
(integration?.scope === "github-org" && "Organization") ||
(["github-repo", "github-env"].includes(integration?.scope as string) &&
@ -153,7 +154,7 @@ export const IntegrationsSection = ({
{(integration.integration === "hashicorp-vault" &&
`${integration.app} - path: ${integration.path}`) ||
(integration.scope === "github-org" && `${integration.owner}`) ||
(integration.integration === "aws-parameter-store" &&
(["aws-parameter-store", "rundeck"].includes(integration.integration) &&
`${integration.path}`) ||
(integration.scope?.startsWith("github-") &&
`${integration.owner}/${integration.app}`) ||

View File

@ -1,17 +1,20 @@
import { FormEvent, useEffect, useState } from "react";
import { FormEvent, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import HCaptcha from "@hcaptcha/react-hcaptcha";
import Error from "@app/components/basic/Error";
import { createNotification } from "@app/components/notifications";
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
import { Button, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { useFetchServerStatus } from "@app/hooks/api";
import { navigateUserToSelectOrg } from "../../Login.utils";
@ -31,21 +34,18 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
const [loginError, setLoginError] = useState(false);
const { config } = useServerConfig();
const queryParams = new URLSearchParams(window.location.search);
const [captchaToken, setCaptchaToken] = useState("");
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
const captchaRef = useRef<HCaptcha>(null);
const { data: serverDetails } = useFetchServerStatus();
useEffect(() => {
if (
process.env.NEXT_PUBLIC_SAML_ORG_SLUG &&
process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default"
) {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/saml2/organizations/${process.env.NEXT_PUBLIC_SAML_ORG_SLUG}${
callbackPort ? `?callback_port=${callbackPort}` : ""
}`
);
window.close();
}
}, []);
if (serverDetails?.samlDefaultOrgSlug){
const callbackPort = queryParams.get("callback_port");
const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${serverDetails?.samlDefaultOrgSlug}${callbackPort ? `?callback_port=${callbackPort}` : ""}`
router.push(redirectUrl);
}
}, [serverDetails?.samlDefaultOrgSlug]);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
@ -61,7 +61,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
// attemptCliLogin
const isCliLoginSuccessful = await attemptCliLogin({
email: email.toLowerCase(),
password
password,
captchaToken
});
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
@ -83,7 +84,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
} else {
const isLoginSuccessful = await attemptLogin({
email: email.toLowerCase(),
password
password,
captchaToken
});
if (isLoginSuccessful && isLoginSuccessful.success) {
@ -117,6 +119,12 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
return;
}
if (err.response.data.error === "Captcha Required") {
setShouldShowCaptcha(true);
setIsLoading(false);
return;
}
setLoginError(true);
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
@ -124,6 +132,11 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
});
}
if (captchaRef.current) {
captchaRef.current.resetCaptcha();
}
setCaptchaToken("");
setIsLoading(false);
};
@ -245,8 +258,19 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
className="select:-webkit-autofill:focus h-10"
/>
</div>
{shouldShowCaptcha && (
<div className="mt-4">
<HCaptcha
theme="dark"
sitekey={CAPTCHA_SITE_KEY}
onVerify={(token) => setCaptchaToken(token)}
ref={captchaRef}
/>
</div>
)}
<div className="mt-3 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
disabled={shouldShowCaptcha && captchaToken === ""}
type="submit"
size="sm"
isFullWidth

View File

@ -1,13 +1,15 @@
import { useState } from "react";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import HCaptcha from "@hcaptcha/react-hcaptcha";
import axios from "axios";
import jwt_decode from "jwt-decode";
import { createNotification } from "@app/components/notifications";
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
import { Button, Input } from "@app/components/v2";
import { useUpdateUserAuthMethods } from "@app/hooks/api";
import { useSelectOrganization } from "@app/hooks/api/auth/queries";
@ -41,6 +43,10 @@ export const PasswordStep = ({
providerAuthToken
) as any;
const [captchaToken, setCaptchaToken] = useState("");
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
const captchaRef = useRef<HCaptcha>(null);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
try {
@ -51,7 +57,8 @@ export const PasswordStep = ({
const isCliLoginSuccessful = await attemptCliLogin({
email,
password,
providerAuthToken
providerAuthToken,
captchaToken
});
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
@ -99,7 +106,8 @@ export const PasswordStep = ({
const loginAttempt = await attemptLogin({
email,
password,
providerAuthToken
providerAuthToken,
captchaToken
});
if (loginAttempt && loginAttempt.success) {
@ -158,11 +166,21 @@ export const PasswordStep = ({
return;
}
if (err.response.data.error === "Captcha Required") {
setShouldShowCaptcha(true);
return;
}
createNotification({
text: "Login unsuccessful. Double-check your master password and try again.",
type: "error"
});
}
if (captchaRef.current) {
captchaRef.current.resetCaptcha();
}
setCaptchaToken("");
};
return (
@ -194,8 +212,19 @@ export const PasswordStep = ({
/>
</div>
</div>
{shouldShowCaptcha && (
<div className="mx-auto mt-4 flex w-full min-w-[22rem] items-center justify-center lg:w-1/6">
<HCaptcha
theme="dark"
sitekey={CAPTCHA_SITE_KEY}
onVerify={(token) => setCaptchaToken(token)}
ref={captchaRef}
/>
</div>
)}
<div className="mx-auto mt-4 flex w-1/4 w-full min-w-[22rem] items-center justify-center rounded-md text-center lg:w-1/6">
<Button
disabled={shouldShowCaptcha && captchaToken === ""}
type="submit"
colorSchema="primary"
variant="outline_bg"

View File

@ -43,6 +43,7 @@ import {
useProjectPermission,
useWorkspace
} from "@app/context";
import { removeTrailingSlash } from "@app/helpers/string";
import { usePopUp } from "@app/hooks";
import {
TProjectUserPrivilege,
@ -104,7 +105,9 @@ export const SpecificPrivilegeSecretForm = ({
? {
environmentSlug: privilege.permissions?.[0]?.conditions?.environment,
// secret path will be inside $glob operator
secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "",
secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob
? removeTrailingSlash(privilege.permissions?.[0]?.conditions?.secretPath?.$glob)
: "",
read: privilege.permissions?.some(({ action }) =>
action.includes(ProjectPermissionActions.Read)
),
@ -183,7 +186,7 @@ export const SpecificPrivilegeSecretForm = ({
];
const conditions: Record<string, any> = { environment: data.environmentSlug };
if (data.secretPath) {
conditions.secretPath = { $glob: data.secretPath };
conditions.secretPath = { $glob: removeTrailingSlash(data.secretPath) };
}
await updateUserPrivilege.mutateAsync({
privilegeId: privilege.id,

View File

@ -393,15 +393,15 @@ export const SecretDetailSidebar = ({
{(isAllowed) => (
<Switch
id="skipmultiencoding-option"
onCheckedChange={(isChecked) => onChange(!isChecked)}
isChecked={!value}
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
onBlur={onBlur}
isDisabled={!isAllowed}
className="items-center"
>
Enable multi line encoding
Multi line encoding
<Tooltip
content="Infisical encodes multiline secrets by escaping newlines and wrapping in quotes. To disable, enable this option"
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />

View File

@ -454,12 +454,12 @@ export const SecretOverviewPage = () => {
const filteredSecretNames = secKeys
?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase()))
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
const filteredFolderNames = folderNames?.filter((name) =>
name.toLowerCase().includes(searchFilter.toLowerCase())
);
const filteredDynamicSecrets = dynamicSecretNames?.filter((name) =>
name.toLowerCase().includes(searchFilter.toLowerCase())
);
const filteredFolderNames = folderNames
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
const filteredDynamicSecrets = dynamicSecretNames
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
const isTableEmpty =
!(

View File

@ -178,7 +178,7 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
errorText={error?.message}
>
<SecretInput
isVisible
isVisible={false}
{...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[100px]"
/>

View File

@ -232,7 +232,6 @@ func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context
}
managedKubeSecret.Data = plainProcessedSecrets
managedKubeSecret.ObjectMeta.Annotations = map[string]string{}
managedKubeSecret.ObjectMeta.Annotations[SECRET_VERSION_ANNOTATION] = ETag
err := r.Client.Update(ctx, &managedKubeSecret)