mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 23:28:31 +00:00
Merge remote-tracking branch 'origin/main' into misc/metrics-observability
This commit is contained in:
@@ -64,5 +64,9 @@ CLIENT_SECRET_GITHUB_LOGIN=
|
|||||||
CLIENT_ID_GITLAB_LOGIN=
|
CLIENT_ID_GITLAB_LOGIN=
|
||||||
CLIENT_SECRET_GITLAB_LOGIN=
|
CLIENT_SECRET_GITLAB_LOGIN=
|
||||||
|
|
||||||
|
CAPTCHA_SECRET=
|
||||||
|
|
||||||
|
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
||||||
|
|
||||||
OTEL_COLLECTOR_OTLP_URL=
|
OTEL_COLLECTOR_OTLP_URL=
|
||||||
OTEL_TELEMETRY_COLLECTION_ENABLED=
|
OTEL_TELEMETRY_COLLECTION_ENABLED=
|
||||||
|
|||||||
@@ -40,13 +40,14 @@ jobs:
|
|||||||
REDIS_URL: redis://172.17.0.1:6379
|
REDIS_URL: redis://172.17.0.1:6379
|
||||||
DB_CONNECTION_URI: postgres://infisical:[email protected]:5432/infisical?sslmode=disable
|
DB_CONNECTION_URI: postgres://infisical:[email protected]:5432/infisical?sslmode=disable
|
||||||
JWT_AUTH_SECRET: something-random
|
JWT_AUTH_SECRET: something-random
|
||||||
|
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21.5'
|
go-version: '1.21.5'
|
||||||
- name: Wait for container to be stable and check logs
|
- name: Wait for container to be stable and check logs
|
||||||
run: |
|
run: |
|
||||||
SECONDS=0
|
SECONDS=0
|
||||||
HEALTHY=0
|
r HEALTHY=0
|
||||||
while [ $SECONDS -lt 60 ]; do
|
while [ $SECONDS -lt 60 ]; do
|
||||||
if docker ps | grep infisical-api | grep -q healthy; then
|
if docker ps | grep infisical-api | grep -q healthy; then
|
||||||
echo "Container is healthy."
|
echo "Container is healthy."
|
||||||
@@ -73,4 +74,4 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker-compose -f "docker-compose.dev.yml" down
|
docker-compose -f "docker-compose.dev.yml" down
|
||||||
docker stop infisical-api
|
docker stop infisical-api
|
||||||
docker remove infisical-api
|
docker remove infisical-api
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
ARG POSTHOG_HOST=https://app.posthog.com
|
ARG POSTHOG_HOST=https://app.posthog.com
|
||||||
ARG POSTHOG_API_KEY=posthog-api-key
|
ARG POSTHOG_API_KEY=posthog-api-key
|
||||||
ARG INTERCOM_ID=intercom-id
|
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
|
FROM node:20-alpine AS base
|
||||||
|
|
||||||
@@ -36,8 +36,8 @@ ARG INTERCOM_ID
|
|||||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
||||||
ARG INFISICAL_PLATFORM_VERSION
|
ARG INFISICAL_PLATFORM_VERSION
|
||||||
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||||
ARG SAML_ORG_SLUG
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
|
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
@@ -113,9 +113,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
|||||||
ARG INTERCOM_ID=intercom-id
|
ARG INTERCOM_ID=intercom-id
|
||||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
||||||
ARG SAML_ORG_SLUG
|
ARG CAPTCHA_SITE_KEY
|
||||||
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \
|
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
||||||
BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
|
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
|
|||||||
@@ -85,13 +85,13 @@ To set up and run Infisical locally, make sure you have Git and Docker installed
|
|||||||
Linux/macOS:
|
Linux/macOS:
|
||||||
|
|
||||||
```console
|
```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:
|
Windows Command Prompt:
|
||||||
|
|
||||||
```console
|
```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`
|
Create an account at `http://localhost:80`
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
|
import { Lock } from "@app/lib/red-lock";
|
||||||
|
|
||||||
export const mockKeyStore = (): TKeyStoreFactory => {
|
export const mockKeyStore = (): TKeyStoreFactory => {
|
||||||
const store: Record<string, string | number | Buffer> = {};
|
const store: Record<string, string | number | Buffer> = {};
|
||||||
@@ -27,7 +28,10 @@ export const mockKeyStore = (): TKeyStoreFactory => {
|
|||||||
return 1;
|
return 1;
|
||||||
},
|
},
|
||||||
acquireLock: () => {
|
acquireLock: () => {
|
||||||
throw new Error("Not implemented");
|
return Promise.resolve({
|
||||||
}
|
release: () => {}
|
||||||
|
}) as Promise<Lock>;
|
||||||
|
},
|
||||||
|
waitTillReady: async () => {}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ const getZodPrimitiveType = (type: string) => {
|
|||||||
return "z.coerce.number()";
|
return "z.coerce.number()";
|
||||||
case "text":
|
case "text":
|
||||||
return "z.string()";
|
return "z.string()";
|
||||||
|
case "bytea":
|
||||||
|
return "zodBuffer";
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid type: ${type}`);
|
throw new Error(`Invalid type: ${type}`);
|
||||||
}
|
}
|
||||||
@@ -96,10 +98,15 @@ const main = async () => {
|
|||||||
const columnNames = Object.keys(columns);
|
const columnNames = Object.keys(columns);
|
||||||
|
|
||||||
let schema = "";
|
let schema = "";
|
||||||
|
const zodImportSet = new Set<string>();
|
||||||
for (let colNum = 0; colNum < columnNames.length; colNum++) {
|
for (let colNum = 0; colNum < columnNames.length; colNum++) {
|
||||||
const columnName = columnNames[colNum];
|
const columnName = columnNames[colNum];
|
||||||
const colInfo = columns[columnName];
|
const colInfo = columns[columnName];
|
||||||
let ztype = getZodPrimitiveType(colInfo.type);
|
let ztype = getZodPrimitiveType(colInfo.type);
|
||||||
|
if (["zodBuffer"].includes(ztype)) {
|
||||||
|
zodImportSet.add(ztype);
|
||||||
|
}
|
||||||
|
|
||||||
// don't put optional on id
|
// don't put optional on id
|
||||||
if (colInfo.defaultValue && columnName !== "id") {
|
if (colInfo.defaultValue && columnName !== "id") {
|
||||||
const { defaultValue } = colInfo;
|
const { defaultValue } = colInfo;
|
||||||
@@ -121,6 +128,8 @@ const main = async () => {
|
|||||||
.split("_")
|
.split("_")
|
||||||
.reduce((prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`, "");
|
.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
|
// the insert and update are changed to zod input type to use default cases
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
|
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
|
||||||
@@ -131,6 +140,8 @@ const main = async () => {
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
${zodImports.length ? `import { ${zodImports.join(",")} } from \"@app/lib/zod\";` : ""}
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const ${pascalCase}Schema = z.object({${schema}});
|
export const ${pascalCase}Schema = z.object({${schema}});
|
||||||
|
|||||||
Vendored
+20
-1
@@ -98,6 +98,15 @@ import {
|
|||||||
TIntegrations,
|
TIntegrations,
|
||||||
TIntegrationsInsert,
|
TIntegrationsInsert,
|
||||||
TIntegrationsUpdate,
|
TIntegrationsUpdate,
|
||||||
|
TKmsKeys,
|
||||||
|
TKmsKeysInsert,
|
||||||
|
TKmsKeysUpdate,
|
||||||
|
TKmsKeyVersions,
|
||||||
|
TKmsKeyVersionsInsert,
|
||||||
|
TKmsKeyVersionsUpdate,
|
||||||
|
TKmsRootConfig,
|
||||||
|
TKmsRootConfigInsert,
|
||||||
|
TKmsRootConfigUpdate,
|
||||||
TLdapConfigs,
|
TLdapConfigs,
|
||||||
TLdapConfigsInsert,
|
TLdapConfigsInsert,
|
||||||
TLdapConfigsUpdate,
|
TLdapConfigsUpdate,
|
||||||
@@ -176,6 +185,9 @@ import {
|
|||||||
TSecretImports,
|
TSecretImports,
|
||||||
TSecretImportsInsert,
|
TSecretImportsInsert,
|
||||||
TSecretImportsUpdate,
|
TSecretImportsUpdate,
|
||||||
|
TSecretReferences,
|
||||||
|
TSecretReferencesInsert,
|
||||||
|
TSecretReferencesUpdate,
|
||||||
TSecretRotationOutputs,
|
TSecretRotationOutputs,
|
||||||
TSecretRotationOutputsInsert,
|
TSecretRotationOutputsInsert,
|
||||||
TSecretRotationOutputsUpdate,
|
TSecretRotationOutputsUpdate,
|
||||||
@@ -240,7 +252,6 @@ import {
|
|||||||
TWebhooksInsert,
|
TWebhooksInsert,
|
||||||
TWebhooksUpdate
|
TWebhooksUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { TSecretReferences, TSecretReferencesInsert, TSecretReferencesUpdate } from "@app/db/schemas/secret-references";
|
|
||||||
|
|
||||||
declare module "knex/types/tables" {
|
declare module "knex/types/tables" {
|
||||||
interface Tables {
|
interface Tables {
|
||||||
@@ -514,5 +525,13 @@ declare module "knex/types/tables" {
|
|||||||
TSecretVersionTagJunctionInsert,
|
TSecretVersionTagJunctionInsert,
|
||||||
TSecretVersionTagJunctionUpdate
|
TSecretVersionTagJunctionUpdate
|
||||||
>;
|
>;
|
||||||
|
// KMS service
|
||||||
|
[TableName.KmsServerRootConfig]: Knex.CompositeTableType<
|
||||||
|
TKmsRootConfig,
|
||||||
|
TKmsRootConfigInsert,
|
||||||
|
TKmsRootConfigUpdate
|
||||||
|
>;
|
||||||
|
[TableName.KmsKey]: Knex.CompositeTableType<TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate>;
|
||||||
|
[TableName.KmsKeyVersion]: Knex.CompositeTableType<TKmsKeyVersions, TKmsKeyVersionsInsert, TKmsKeyVersionsUpdate>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
+29
@@ -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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -30,6 +30,9 @@ export * from "./identity-universal-auths";
|
|||||||
export * from "./incident-contacts";
|
export * from "./incident-contacts";
|
||||||
export * from "./integration-auths";
|
export * from "./integration-auths";
|
||||||
export * from "./integrations";
|
export * from "./integrations";
|
||||||
|
export * from "./kms-key-versions";
|
||||||
|
export * from "./kms-keys";
|
||||||
|
export * from "./kms-root-config";
|
||||||
export * from "./ldap-configs";
|
export * from "./ldap-configs";
|
||||||
export * from "./ldap-group-maps";
|
export * from "./ldap-group-maps";
|
||||||
export * from "./models";
|
export * from "./models";
|
||||||
@@ -57,6 +60,7 @@ export * from "./secret-blind-indexes";
|
|||||||
export * from "./secret-folder-versions";
|
export * from "./secret-folder-versions";
|
||||||
export * from "./secret-folders";
|
export * from "./secret-folders";
|
||||||
export * from "./secret-imports";
|
export * from "./secret-imports";
|
||||||
|
export * from "./secret-references";
|
||||||
export * from "./secret-rotation-outputs";
|
export * from "./secret-rotation-outputs";
|
||||||
export * from "./secret-rotations";
|
export * from "./secret-rotations";
|
||||||
export * from "./secret-scanning-git-risks";
|
export * from "./secret-scanning-git-risks";
|
||||||
|
|||||||
@@ -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>>;
|
||||||
@@ -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>>;
|
||||||
@@ -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>>;
|
||||||
@@ -81,7 +81,11 @@ export enum TableName {
|
|||||||
DynamicSecretLease = "dynamic_secret_leases",
|
DynamicSecretLease = "dynamic_secret_leases",
|
||||||
// junction tables with tags
|
// junction tables with tags
|
||||||
JnSecretTag = "secret_tag_junction",
|
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";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export const UsersSchema = z.object({
|
|||||||
isEmailVerified: z.boolean().default(false).nullable().optional(),
|
isEmailVerified: z.boolean().default(false).nullable().optional(),
|
||||||
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
|
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
|
||||||
isLocked: z.boolean().default(false).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>;
|
export type TUsers = z.infer<typeof UsersSchema>;
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ type TLdapConfigServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
|
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
|
||||||
@@ -510,6 +510,7 @@ export const ldapConfigServiceFactory = ({
|
|||||||
return newUserAlias;
|
return newUserAlias;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||||
|
|
||||||
const user = await userDAL.transaction(async (tx) => {
|
const user = await userDAL.transaction(async (tx) => {
|
||||||
const newUser = await userDAL.findOne({ id: userAlias.userId }, tx);
|
const newUser = await userDAL.findOne({ id: userAlias.userId }, tx);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ type TSamlConfigServiceFactoryDep = {
|
|||||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
||||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
};
|
};
|
||||||
@@ -449,6 +449,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
return newUser;
|
return newUser;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||||
|
|
||||||
const isUserCompleted = Boolean(user.isAccepted);
|
const isUserCompleted = Boolean(user.isAccepted);
|
||||||
const providerAuthToken = jwt.sign(
|
const providerAuthToken = jwt.sign(
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ export const scimServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(org.id);
|
||||||
return { user, orgMembership };
|
return { user, orgMembership };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ export enum KeyStorePrefixes {
|
|||||||
SecretReplication = "secret-replication-import-lock"
|
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) => {
|
export const keyStoreFactory = (redisUrl: string) => {
|
||||||
const redis = new Redis(redisUrl);
|
const redis = new Redis(redisUrl);
|
||||||
const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 });
|
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 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 {
|
return {
|
||||||
setItem,
|
setItem,
|
||||||
getItem,
|
getItem,
|
||||||
@@ -37,6 +69,7 @@ export const keyStoreFactory = (redisUrl: string) => {
|
|||||||
incrementBy,
|
incrementBy,
|
||||||
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
|
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
|
||||||
return redisLock.acquire(resources, duration, settings);
|
return redisLock.acquire(resources, duration, settings);
|
||||||
}
|
},
|
||||||
|
waitTillReady
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -386,6 +386,8 @@ export const SECRET_IMPORTS = {
|
|||||||
environment: "The slug of the environment to import into.",
|
environment: "The slug of the environment to import into.",
|
||||||
path: "The path to import into.",
|
path: "The path to import into.",
|
||||||
workspaceId: "The ID of the project you are working in.",
|
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: {
|
import: {
|
||||||
environment: "The slug of the environment to import from.",
|
environment: "The slug of the environment to import from.",
|
||||||
path: "The path to import from."
|
path: "The path to import from."
|
||||||
@@ -661,6 +663,7 @@ export const INTEGRATION = {
|
|||||||
targetServiceId:
|
targetServiceId:
|
||||||
"The service based grouping identifier ID of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank",
|
"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.",
|
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",
|
path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault",
|
||||||
region: "AWS region to sync secrets to.",
|
region: "AWS region to sync secrets to.",
|
||||||
scope: "Scope of the provider. Used by Github, Qovery",
|
scope: "Scope of the provider. Used by Github, Qovery",
|
||||||
@@ -673,7 +676,10 @@ export const INTEGRATION = {
|
|||||||
secretGCPLabel: "The label for GCP secrets.",
|
secretGCPLabel: "The label for GCP secrets.",
|
||||||
secretAWSTag: "The tags for AWS secrets.",
|
secretAWSTag: "The tags for AWS secrets.",
|
||||||
kmsKeyId: "The ID of the encryption key from AWS KMS.",
|
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.",
|
||||||
|
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",
|
||||||
|
shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.",
|
||||||
|
shouldEnableDelete: "The flag to enable deletion of secrets"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
UPDATE: {
|
UPDATE: {
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ const envSchema = z
|
|||||||
HTTPS_ENABLED: zodStrBool,
|
HTTPS_ENABLED: zodStrBool,
|
||||||
// smtp options
|
// smtp options
|
||||||
SMTP_HOST: zpStr(z.string().optional()),
|
SMTP_HOST: zpStr(z.string().optional()),
|
||||||
SMTP_SECURE: zodStrBool,
|
SMTP_IGNORE_TLS: zodStrBool.default("false"),
|
||||||
|
SMTP_REQUIRE_TLS: zodStrBool.default("true"),
|
||||||
|
SMTP_TLS_REJECT_UNAUTHORIZED: zodStrBool.default("true"),
|
||||||
SMTP_PORT: z.coerce.number().default(587),
|
SMTP_PORT: z.coerce.number().default(587),
|
||||||
SMTP_USERNAME: zpStr(z.string().optional()),
|
SMTP_USERNAME: zpStr(z.string().optional()),
|
||||||
SMTP_PASSWORD: zpStr(z.string().optional()),
|
SMTP_PASSWORD: zpStr(z.string().optional()),
|
||||||
@@ -75,6 +77,7 @@ const envSchema = z
|
|||||||
.optional()
|
.optional()
|
||||||
.default(process.env.URL_GITLAB_LOGIN ?? GITLAB_URL)
|
.default(process.env.URL_GITLAB_LOGIN ?? GITLAB_URL)
|
||||||
), // fallback since URL_GITLAB_LOGIN has been renamed
|
), // 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
|
// integration client secrets
|
||||||
// heroku
|
// heroku
|
||||||
CLIENT_ID_HEROKU: zpStr(z.string().optional()),
|
CLIENT_ID_HEROKU: zpStr(z.string().optional()),
|
||||||
@@ -120,6 +123,7 @@ const envSchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
INFISICAL_CLOUD: zodStrBool.default("false"),
|
INFISICAL_CLOUD: zodStrBool.default("false"),
|
||||||
MAINTENANCE_MODE: zodStrBool.default("false"),
|
MAINTENANCE_MODE: zodStrBool.default("false"),
|
||||||
|
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
||||||
OTEL_TELEMETRY_COLLECTION_ENABLED: zodStrBool.default("false"),
|
OTEL_TELEMETRY_COLLECTION_ENABLED: zodStrBool.default("false"),
|
||||||
OTEL_COLLECTOR_OTLP_URL: zpStr(z.string().optional())
|
OTEL_COLLECTOR_OTLP_URL: zpStr(z.string().optional())
|
||||||
})
|
})
|
||||||
@@ -133,7 +137,8 @@ const envSchema = z
|
|||||||
isSecretScanningConfigured:
|
isSecretScanningConfigured:
|
||||||
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
|
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
|
||||||
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
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>>;
|
let envCfg: Readonly<z.infer<typeof envSchema>>;
|
||||||
@@ -152,13 +157,20 @@ export const initEnvConfig = (logger: Logger) => {
|
|||||||
return envCfg;
|
return envCfg;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatSmtpConfig = () => ({
|
export const formatSmtpConfig = () => {
|
||||||
host: envCfg.SMTP_HOST,
|
return {
|
||||||
port: envCfg.SMTP_PORT,
|
host: envCfg.SMTP_HOST,
|
||||||
auth:
|
port: envCfg.SMTP_PORT,
|
||||||
envCfg.SMTP_USERNAME && envCfg.SMTP_PASSWORD
|
auth:
|
||||||
? { user: envCfg.SMTP_USERNAME, pass: envCfg.SMTP_PASSWORD }
|
envCfg.SMTP_USERNAME && envCfg.SMTP_PASSWORD
|
||||||
: undefined,
|
? { user: envCfg.SMTP_USERNAME, pass: envCfg.SMTP_PASSWORD }
|
||||||
secure: envCfg.SMTP_SECURE,
|
: undefined,
|
||||||
from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`
|
secure: envCfg.SMTP_PORT === 465,
|
||||||
});
|
from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`,
|
||||||
|
ignoreTLS: envCfg.SMTP_IGNORE_TLS,
|
||||||
|
requireTLS: envCfg.SMTP_REQUIRE_TLS,
|
||||||
|
tls: {
|
||||||
|
rejectUnauthorized: envCfg.SMTP_TLS_REJECT_UNAUTHORIZED
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { symmetricCipherService } from "./cipher";
|
||||||
|
export { SymmetricEncryption } from "./types";
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -11,6 +11,8 @@ import { getConfig } from "../config/env";
|
|||||||
export const decodeBase64 = (s: string) => naclUtils.decodeBase64(s);
|
export const decodeBase64 = (s: string) => naclUtils.decodeBase64(s);
|
||||||
export const encodeBase64 = (u: Uint8Array) => naclUtils.encodeBase64(u);
|
export const encodeBase64 = (u: Uint8Array) => naclUtils.encodeBase64(u);
|
||||||
|
|
||||||
|
export const randomSecureBytes = (length = 32) => crypto.randomBytes(length);
|
||||||
|
|
||||||
export type TDecryptSymmetricInput = {
|
export type TDecryptSymmetricInput = {
|
||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export {
|
|||||||
encryptAsymmetric,
|
encryptAsymmetric,
|
||||||
encryptSymmetric,
|
encryptSymmetric,
|
||||||
encryptSymmetric128BitHexKeyUTF8,
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
generateAsymmetricKeyPair
|
generateAsymmetricKeyPair,
|
||||||
|
randomSecureBytes
|
||||||
} from "./encryption";
|
} from "./encryption";
|
||||||
export {
|
export {
|
||||||
decryptIntegrationAuths,
|
decryptIntegrationAuths,
|
||||||
|
|||||||
@@ -7,3 +7,7 @@ export const zpStr = <T extends ZodTypeAny>(schema: T, opt: { stripNull: boolean
|
|||||||
if (typeof val !== "string") return val;
|
if (typeof val !== "string") return val;
|
||||||
return val.trim() || undefined;
|
return val.trim() || undefined;
|
||||||
}, schema);
|
}, schema);
|
||||||
|
|
||||||
|
export const zodBuffer = z.custom<Buffer>((data) => Buffer.isBuffer(data) || data instanceof Uint8Array, {
|
||||||
|
message: "Expected binary data (Buffer Or Uint8Array)"
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { createTransport } from "nodemailer";
|
|||||||
|
|
||||||
import { formatSmtpConfig, getConfig } from "@app/lib/config/env";
|
import { formatSmtpConfig, getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { getTlsOption } from "@app/services/smtp/smtp-service";
|
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
|
|
||||||
type BootstrapOpt = {
|
type BootstrapOpt = {
|
||||||
@@ -44,7 +43,7 @@ export const bootstrapCheck = async ({ db }: BootstrapOpt) => {
|
|||||||
console.info("Testing smtp connection");
|
console.info("Testing smtp connection");
|
||||||
|
|
||||||
const smtpCfg = formatSmtpConfig();
|
const smtpCfg = formatSmtpConfig();
|
||||||
await createTransport({ ...smtpCfg, ...getTlsOption(smtpCfg.host, smtpCfg.secure) })
|
await createTransport(smtpCfg)
|
||||||
.verify()
|
.verify()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
console.info("SMTP successfully connected");
|
console.info("SMTP successfully connected");
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ import { integrationDALFactory } from "@app/services/integration/integration-dal
|
|||||||
import { integrationServiceFactory } from "@app/services/integration/integration-service";
|
import { integrationServiceFactory } from "@app/services/integration/integration-service";
|
||||||
import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal";
|
import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal";
|
||||||
import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
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 { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
||||||
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
import { orgDALFactory } from "@app/services/org/org-dal";
|
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||||
@@ -261,6 +264,9 @@ export const registerRoutes = async (
|
|||||||
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
||||||
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
||||||
|
|
||||||
|
const kmsDAL = kmsDALFactory(db);
|
||||||
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
||||||
|
|
||||||
const permissionService = permissionServiceFactory({
|
const permissionService = permissionServiceFactory({
|
||||||
permissionDAL,
|
permissionDAL,
|
||||||
orgRoleDAL,
|
orgRoleDAL,
|
||||||
@@ -269,6 +275,12 @@ export const registerRoutes = async (
|
|||||||
projectDAL
|
projectDAL
|
||||||
});
|
});
|
||||||
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
|
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
|
||||||
|
const kmsService = kmsServiceFactory({
|
||||||
|
kmsRootConfigDAL,
|
||||||
|
keyStore,
|
||||||
|
kmsDAL
|
||||||
|
});
|
||||||
|
|
||||||
const trustedIpService = trustedIpServiceFactory({
|
const trustedIpService = trustedIpServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -823,6 +835,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
await telemetryQueue.startTelemetryCheck();
|
await telemetryQueue.startTelemetryCheck();
|
||||||
await dailyResourceCleanUp.startCleanUp();
|
await dailyResourceCleanUp.startCleanUp();
|
||||||
|
await kmsService.startService();
|
||||||
|
|
||||||
// inject all services
|
// inject all services
|
||||||
server.decorate<FastifyZodProvider["services"]>("services", {
|
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||||
@@ -906,7 +919,8 @@ export const registerRoutes = async (
|
|||||||
emailConfigured: z.boolean().optional(),
|
emailConfigured: z.boolean().optional(),
|
||||||
inviteOnlySignup: z.boolean().optional(),
|
inviteOnlySignup: z.boolean().optional(),
|
||||||
redisConfigured: 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,
|
emailConfigured: cfg.isSmtpConfigured,
|
||||||
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
|
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
|
||||||
redisConfigured: cfg.isRedisConfigured,
|
redisConfigured: cfg.isRedisConfigured,
|
||||||
secretScanningConfigured: cfg.isSecretScanningConfigured
|
secretScanningConfigured: cfg.isSecretScanningConfigured,
|
||||||
|
samlDefaultOrgSlug: cfg.samlDefaultOrgSlug
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { writeLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
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";
|
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
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),
|
targetService: z.string().trim().optional().describe(INTEGRATION.CREATE.targetService),
|
||||||
targetServiceId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetServiceId),
|
targetServiceId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetServiceId),
|
||||||
owner: z.string().trim().optional().describe(INTEGRATION.CREATE.owner),
|
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),
|
path: z.string().trim().optional().describe(INTEGRATION.CREATE.path),
|
||||||
region: z.string().trim().optional().describe(INTEGRATION.CREATE.region),
|
region: z.string().trim().optional().describe(INTEGRATION.CREATE.region),
|
||||||
scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope),
|
scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope),
|
||||||
metadata: z
|
metadata: IntegrationMetadataSchema.default({})
|
||||||
.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({})
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -160,33 +132,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
|||||||
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
|
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
|
||||||
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
|
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
|
||||||
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment),
|
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment),
|
||||||
metadata: z
|
metadata: IntegrationMetadataSchema.optional()
|
||||||
.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()
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
|
|||||||
environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.import.environment),
|
environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.import.environment),
|
||||||
path: z.string().trim().transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.import.path)
|
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: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
email: z.string().trim(),
|
email: z.string().trim(),
|
||||||
providerAuthToken: z.string().trim().optional(),
|
providerAuthToken: z.string().trim().optional(),
|
||||||
clientProof: z.string().trim()
|
clientProof: z.string().trim(),
|
||||||
|
captchaToken: z.string().trim().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.discriminatedUnion("mfaEnabled", [
|
200: z.discriminatedUnion("mfaEnabled", [
|
||||||
@@ -106,6 +107,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
|||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const data = await server.services.login.loginExchangeClientProof({
|
const data = await server.services.login.loginExchangeClientProof({
|
||||||
|
captchaToken: req.body.captchaToken,
|
||||||
email: req.body.email,
|
email: req.body.email,
|
||||||
ip: req.realIp,
|
ip: req.realIp,
|
||||||
userAgent,
|
userAgent,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
|
|||||||
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
@@ -176,12 +177,16 @@ export const authLoginServiceFactory = ({
|
|||||||
clientProof,
|
clientProof,
|
||||||
ip,
|
ip,
|
||||||
userAgent,
|
userAgent,
|
||||||
providerAuthToken
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
}: TLoginClientProofDTO) => {
|
}: TLoginClientProofDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const userEnc = await userDAL.findUserEncKeyByUsername({
|
const userEnc = await userDAL.findUserEncKeyByUsername({
|
||||||
username: email
|
username: email
|
||||||
});
|
});
|
||||||
if (!userEnc) throw new Error("Failed to find user");
|
if (!userEnc) throw new Error("Failed to find user");
|
||||||
|
const user = await userDAL.findById(userEnc.userId);
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
|
|
||||||
let authMethod = AuthMethod.EMAIL;
|
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?");
|
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
|
||||||
const isValidClientProof = await srpCheckClientProof(
|
const isValidClientProof = await srpCheckClientProof(
|
||||||
userEnc.salt,
|
userEnc.salt,
|
||||||
@@ -204,15 +234,31 @@ export const authLoginServiceFactory = ({
|
|||||||
userEnc.clientPublicKey,
|
userEnc.clientPublicKey,
|
||||||
clientProof
|
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, {
|
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
|
||||||
serverPrivateKey: null,
|
serverPrivateKey: null,
|
||||||
clientPublicKey: null
|
clientPublicKey: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await userDAL.updateById(userEnc.userId, {
|
||||||
|
consecutiveFailedPasswordAttempts: 0
|
||||||
|
});
|
||||||
|
|
||||||
// send multi factor auth token if they it enabled
|
// send multi factor auth token if they it enabled
|
||||||
if (userEnc.isMfaEnabled && userEnc.email) {
|
if (userEnc.isMfaEnabled && userEnc.email) {
|
||||||
const user = await userDAL.findById(userEnc.userId);
|
|
||||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
|
|
||||||
const mfaToken = jwt.sign(
|
const mfaToken = jwt.sign(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type TLoginClientProofDTO = {
|
|||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
|
captchaToken?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TVerifyMfaTokenDTO = {
|
export type TVerifyMfaTokenDTO = {
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ export const integrationAuthServiceFactory = ({
|
|||||||
projectId,
|
projectId,
|
||||||
namespace,
|
namespace,
|
||||||
integration,
|
integration,
|
||||||
|
url,
|
||||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||||
keyEncoding: SecretKeyEncoding.UTF8,
|
keyEncoding: SecretKeyEncoding.UTF8,
|
||||||
...(integration === Integrations.GCP_SECRET_MANAGER
|
...(integration === Integrations.GCP_SECRET_MANAGER
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export enum Integrations {
|
|||||||
DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform",
|
DIGITAL_OCEAN_APP_PLATFORM = "digital-ocean-app-platform",
|
||||||
CLOUD_66 = "cloud-66",
|
CLOUD_66 = "cloud-66",
|
||||||
NORTHFLANK = "northflank",
|
NORTHFLANK = "northflank",
|
||||||
HASURA_CLOUD = "hasura-cloud"
|
HASURA_CLOUD = "hasura-cloud",
|
||||||
|
RUNDECK = "rundeck"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum IntegrationType {
|
export enum IntegrationType {
|
||||||
@@ -368,6 +369,15 @@ export const getIntegrationOptions = async () => {
|
|||||||
type: "pat",
|
type: "pat",
|
||||||
clientId: "",
|
clientId: "",
|
||||||
docsLink: ""
|
docsLink: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Rundeck",
|
||||||
|
slug: "rundeck",
|
||||||
|
image: "Rundeck.svg",
|
||||||
|
isAvailable: true,
|
||||||
|
type: "pat",
|
||||||
|
clientId: "",
|
||||||
|
docsLink: ""
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
|
import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||||
|
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||||
import {
|
import {
|
||||||
IntegrationInitialSyncBehavior,
|
IntegrationInitialSyncBehavior,
|
||||||
IntegrationMappingBehavior,
|
IntegrationMappingBehavior,
|
||||||
@@ -1363,38 +1364,41 @@ const syncSecretsGitHub = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const encryptedSecret of encryptedSecrets) {
|
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||||
if (
|
if (metadata.shouldEnableDelete) {
|
||||||
!(encryptedSecret.name in secrets) &&
|
for await (const encryptedSecret of encryptedSecrets) {
|
||||||
!(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) &&
|
if (
|
||||||
!(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix))
|
!(encryptedSecret.name in secrets) &&
|
||||||
) {
|
!(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) &&
|
||||||
switch (integration.scope) {
|
!(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix))
|
||||||
case GithubScope.Org: {
|
) {
|
||||||
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
|
switch (integration.scope) {
|
||||||
org: integration.owner as string,
|
case GithubScope.Org: {
|
||||||
secret_name: encryptedSecret.name
|
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
|
||||||
});
|
org: integration.owner as string,
|
||||||
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
|
secret_name: encryptedSecret.name
|
||||||
}
|
});
|
||||||
);
|
break;
|
||||||
break;
|
}
|
||||||
}
|
case GithubScope.Env: {
|
||||||
default: {
|
await octokit.request(
|
||||||
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
|
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
|
||||||
owner: integration.owner as string,
|
{
|
||||||
repo: integration.app as string,
|
repository_id: Number(integration.appId),
|
||||||
secret_name: encryptedSecret.name
|
environment_name: integration.targetEnvironmentId as string,
|
||||||
});
|
secret_name: encryptedSecret.name
|
||||||
break;
|
}
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1917,13 +1921,13 @@ const syncSecretsGitLab = async ({
|
|||||||
return allEnvVariables;
|
return allEnvVariables;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||||
const allEnvVariables = await getAllEnvVariables(integration?.appId as string, accessToken);
|
const allEnvVariables = await getAllEnvVariables(integration?.appId as string, accessToken);
|
||||||
const getSecretsRes: GitLabSecret[] = allEnvVariables
|
const getSecretsRes: GitLabSecret[] = allEnvVariables
|
||||||
.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
|
.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
|
||||||
.filter((gitLabSecret) => {
|
.filter((gitLabSecret) => {
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
|
|
||||||
const metadata = z.record(z.any()).parse(integration.metadata);
|
|
||||||
if (metadata.secretPrefix && !gitLabSecret.key.startsWith(metadata.secretPrefix)) {
|
if (metadata.secretPrefix && !gitLabSecret.key.startsWith(metadata.secretPrefix)) {
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
@@ -1943,8 +1947,8 @@ const syncSecretsGitLab = async ({
|
|||||||
{
|
{
|
||||||
key,
|
key,
|
||||||
value: secrets[key].value,
|
value: secrets[key].value,
|
||||||
protected: false,
|
protected: Boolean(metadata.shouldProtectSecrets),
|
||||||
masked: false,
|
masked: Boolean(metadata.shouldMaskSecrets),
|
||||||
raw: false,
|
raw: false,
|
||||||
environment_scope: integration.targetEnvironment
|
environment_scope: integration.targetEnvironment
|
||||||
},
|
},
|
||||||
@@ -1961,7 +1965,9 @@ const syncSecretsGitLab = async ({
|
|||||||
`${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
|
`${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
|
||||||
{
|
{
|
||||||
...existingSecret,
|
...existingSecret,
|
||||||
value: secrets[existingSecret.key].value
|
value: secrets[existingSecret.key].value,
|
||||||
|
protected: Boolean(metadata.shouldProtectSecrets),
|
||||||
|
masked: Boolean(metadata.shouldMaskSecrets)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
@@ -2750,6 +2756,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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3355,6 +3375,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]
|
* Sync/push [secrets] to [app] in integration named [integration]
|
||||||
*
|
*
|
||||||
@@ -3621,6 +3717,13 @@ export const syncIntegrationSecrets = async ({
|
|||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case Integrations.RUNDECK:
|
||||||
|
await syncSecretsRundeck({
|
||||||
|
integration,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new BadRequestError({ message: "Invalid integration" });
|
throw new BadRequestError({ message: "Invalid integration" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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),
|
||||||
|
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),
|
||||||
|
shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets)
|
||||||
|
});
|
||||||
@@ -43,6 +43,7 @@ export const integrationServiceFactory = ({
|
|||||||
scope,
|
scope,
|
||||||
actorId,
|
actorId,
|
||||||
region,
|
region,
|
||||||
|
url,
|
||||||
isActive,
|
isActive,
|
||||||
metadata,
|
metadata,
|
||||||
secretPath,
|
secretPath,
|
||||||
@@ -87,6 +88,7 @@ export const integrationServiceFactory = ({
|
|||||||
region,
|
region,
|
||||||
scope,
|
scope,
|
||||||
owner,
|
owner,
|
||||||
|
url,
|
||||||
appId,
|
appId,
|
||||||
path,
|
path,
|
||||||
app,
|
app,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type TCreateIntegrationDTO = {
|
|||||||
targetService?: string;
|
targetService?: string;
|
||||||
targetServiceId?: string;
|
targetServiceId?: string;
|
||||||
owner?: string;
|
owner?: string;
|
||||||
|
url?: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
scope?: string;
|
scope?: string;
|
||||||
@@ -28,6 +29,9 @@ export type TCreateIntegrationDTO = {
|
|||||||
}[];
|
}[];
|
||||||
kmsKeyId?: string;
|
kmsKeyId?: string;
|
||||||
shouldDisableDelete?: boolean;
|
shouldDisableDelete?: boolean;
|
||||||
|
shouldMaskSecrets?: boolean;
|
||||||
|
shouldProtectSecrets?: boolean;
|
||||||
|
shouldEnableDelete?: boolean;
|
||||||
};
|
};
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
@@ -53,6 +57,7 @@ export type TUpdateIntegrationDTO = {
|
|||||||
}[];
|
}[];
|
||||||
kmsKeyId?: string;
|
kmsKeyId?: string;
|
||||||
shouldDisableDelete?: boolean;
|
shouldDisableDelete?: boolean;
|
||||||
|
shouldEnableDelete?: boolean;
|
||||||
};
|
};
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -315,6 +315,7 @@ export const orgServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(org.id);
|
||||||
await orgBotDAL.create(
|
await orgBotDAL.create(
|
||||||
{
|
{
|
||||||
name: org.name,
|
name: org.name,
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
|
|||||||
};
|
};
|
||||||
|
|
||||||
const expandSecrets = async (
|
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 expandedSec: Record<string, string> = {};
|
||||||
const interpolatedSec: 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
|
// should not do multi line encoding if user has set it to skip
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
secrets[key].value = secrets[key].skipMultilineEncoding
|
secrets[key].value = secrets[key].skipMultilineEncoding
|
||||||
? expandedSec[key]
|
? formatMultiValueEnv(expandedSec[key])
|
||||||
: formatMultiValueEnv(expandedSec[key]);
|
: expandedSec[key];
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -347,7 +347,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
|
|||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
secrets[key].value = secrets[key].skipMultilineEncoding ? expandedVal : formatMultiValueEnv(expandedVal);
|
secrets[key].value = secrets[key].skipMultilineEncoding ? formatMultiValueEnv(expandedVal) : expandedVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
return secrets;
|
return secrets;
|
||||||
@@ -395,7 +395,8 @@ export const decryptSecretRaw = (
|
|||||||
type: secret.type,
|
type: secret.type,
|
||||||
_id: secret.id,
|
_id: secret.id,
|
||||||
id: secret.id,
|
id: secret.id,
|
||||||
user: secret.userId
|
user: secret.userId,
|
||||||
|
skipMultilineEncoding: secret.skipMultilineEncoding
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||||
@@ -67,7 +69,10 @@ const MAX_SYNC_SECRET_DEPTH = 5;
|
|||||||
export const uniqueSecretQueueKey = (environment: string, secretPath: string) =>
|
export const uniqueSecretQueueKey = (environment: string, secretPath: string) =>
|
||||||
`secret-queue-dedupe-${environment}-${secretPath}`;
|
`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 = ({
|
export const secretQueueFactory = ({
|
||||||
queueService,
|
queueService,
|
||||||
integrationDAL,
|
integrationDAL,
|
||||||
@@ -567,11 +572,14 @@ export const secretQueueFactory = ({
|
|||||||
isSynced: true
|
isSynced: true
|
||||||
});
|
});
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
logger.info("Secret integration sync error:", err);
|
logger.info("Secret integration sync error: %o", err);
|
||||||
|
const message =
|
||||||
|
err instanceof AxiosError ? JSON.stringify((err as AxiosError)?.response?.data) : (err as Error)?.message;
|
||||||
|
|
||||||
await integrationDAL.updateById(integration.id, {
|
await integrationDAL.updateById(integration.id, {
|
||||||
lastSyncJobId: job.id,
|
lastSyncJobId: job.id,
|
||||||
lastUsed: new Date(),
|
lastUsed: new Date(),
|
||||||
syncMessage: (err as Error)?.message,
|
syncMessage: message,
|
||||||
isSynced: false
|
isSynced: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -952,15 +952,49 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
|
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
|
||||||
const decryptedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({
|
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
|
||||||
...el,
|
const decryptedImportSecrets = importedSecrets.map((sec) =>
|
||||||
secrets: importedSecrets.map((sec) =>
|
|
||||||
decryptSecretRaw(
|
decryptSecretRaw(
|
||||||
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
|
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
|
||||||
botKey
|
botKey
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
}));
|
|
||||||
|
// secret-override to handle duplicate keys from different import levels
|
||||||
|
// this prioritizes secret values from direct imports
|
||||||
|
const importedKeys = new Set<string>();
|
||||||
|
const importedEntries = decryptedImportSecrets.reduce(
|
||||||
|
(
|
||||||
|
accum: {
|
||||||
|
secretKey: string;
|
||||||
|
secretPath: string;
|
||||||
|
workspace: string;
|
||||||
|
environment: string;
|
||||||
|
secretValue: string;
|
||||||
|
secretComment: string;
|
||||||
|
version: number;
|
||||||
|
type: string;
|
||||||
|
_id: string;
|
||||||
|
id: string;
|
||||||
|
user: string | null | undefined;
|
||||||
|
skipMultilineEncoding: boolean | null | undefined;
|
||||||
|
}[],
|
||||||
|
sec
|
||||||
|
) => {
|
||||||
|
if (!importedKeys.has(sec.secretKey)) {
|
||||||
|
importedKeys.add(sec.secretKey);
|
||||||
|
return [...accum, sec];
|
||||||
|
}
|
||||||
|
return accum;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...el,
|
||||||
|
secrets: importedEntries
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (expandSecretReferences) {
|
if (expandSecretReferences) {
|
||||||
const expandSecrets = interpolateSecrets({
|
const expandSecrets = interpolateSecrets({
|
||||||
@@ -971,10 +1005,24 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const batchSecretsExpand = async (
|
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
|
// 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) => {
|
secretBatch.forEach((secret) => {
|
||||||
if (!secretsByPath[secret.secretPath]) {
|
if (!secretsByPath[secret.secretPath]) {
|
||||||
@@ -990,11 +1038,15 @@ export const secretServiceFactory = ({
|
|||||||
continue;
|
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) => {
|
secretsByPath[secPath].forEach((decryptedSecret) => {
|
||||||
secretRecord[decryptedSecret.secretKey] = {
|
secretRecord[decryptedSecret.secretKey] = {
|
||||||
value: decryptedSecret.secretValue,
|
value: decryptedSecret.secretValue,
|
||||||
comment: decryptedSecret.secretComment
|
comment: decryptedSecret.secretComment,
|
||||||
|
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1011,12 +1063,12 @@ export const secretServiceFactory = ({
|
|||||||
await batchSecretsExpand(decryptedSecrets);
|
await batchSecretsExpand(decryptedSecrets);
|
||||||
|
|
||||||
// expand imports by batch
|
// expand imports by batch
|
||||||
await Promise.all(decryptedImports.map((decryptedImport) => batchSecretsExpand(decryptedImport.secrets)));
|
await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
secrets: decryptedSecrets,
|
secrets: decryptedSecrets,
|
||||||
imports: decryptedImports
|
imports: processedImports
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -41,21 +41,8 @@ export enum SmtpHost {
|
|||||||
Office365 = "smtp.office365.com"
|
Office365 = "smtp.office365.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTlsOption = (host?: SmtpHost | string, secure?: boolean) => {
|
|
||||||
if (!secure) return { secure: false };
|
|
||||||
if (!host) return { secure: true };
|
|
||||||
|
|
||||||
if ((host as SmtpHost) === SmtpHost.Sendgrid) {
|
|
||||||
return { secure: true, port: 465 }; // more details here https://nodemailer.com/smtp/
|
|
||||||
}
|
|
||||||
if (host.includes("amazonaws.com")) {
|
|
||||||
return { tls: { ciphers: "TLSv1.2" } };
|
|
||||||
}
|
|
||||||
return { requireTLS: true, tls: { ciphers: "TLSv1.2" } };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
export const smtpServiceFactory = (cfg: TSmtpConfig) => {
|
||||||
const smtp = createTransport({ ...cfg, ...getTlsOption(cfg.host, cfg.secure) });
|
const smtp = createTransport(cfg);
|
||||||
const isSmtpOn = Boolean(cfg.host);
|
const isSmtpOn = Boolean(cfg.host);
|
||||||
|
|
||||||
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
|
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
|
||||||
|
|||||||
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 |
@@ -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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Navigate to your project's integrations tab in Infisical.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Press on the Rundeck tile and input your Rundeck instance Base URL and User API token to grant Infisical access to manage Rundeck keys
|
||||||
|
|
||||||
|

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

|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
@@ -26,14 +26,14 @@ Missing an integration? [Throw in a request](https://github.com/Infisical/infisi
|
|||||||
| [Supabase](/integrations/cloud/supabase) | Cloud | Available |
|
| [Supabase](/integrations/cloud/supabase) | Cloud | Available |
|
||||||
| [Northflank](/integrations/cloud/northflank) | Cloud | Available |
|
| [Northflank](/integrations/cloud/northflank) | Cloud | Available |
|
||||||
| [Cloudflare Pages](/integrations/cloud/cloudflare-pages) | 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 |
|
| [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 |
|
| [HashiCorp Vault](/integrations/cloud/hashicorp-vault) | Cloud | Available |
|
||||||
| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | 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 |
|
| [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 |
|
| [Windmill](/integrations/cloud/windmill) | Cloud | Available |
|
||||||
| [BitBucket](/integrations/cicd/bitbucket) | CI/CD | Available |
|
| [BitBucket](/integrations/cicd/bitbucket) | CI/CD | Available |
|
||||||
| [Codefresh](/integrations/cicd/codefresh) | 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 |
|
| [GitLab](/integrations/cicd/gitlab) | CI/CD | Available |
|
||||||
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available |
|
| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available |
|
||||||
| [Travis CI](/integrations/cicd/travisci) | CI/CD | Available |
|
| [Travis CI](/integrations/cicd/travisci) | CI/CD | Available |
|
||||||
|
| [Rundeck](/integrations/cicd/rundeck) | CI/CD | Available |
|
||||||
| [React](/integrations/frameworks/react) | Framework | Available |
|
| [React](/integrations/frameworks/react) | Framework | Available |
|
||||||
| [Vue](/integrations/frameworks/vue) | Framework | Available |
|
| [Vue](/integrations/frameworks/vue) | Framework | Available |
|
||||||
| [Express](/integrations/frameworks/express) | Framework | Available |
|
| [Express](/integrations/frameworks/express) | Framework | Available |
|
||||||
|
|||||||
@@ -496,7 +496,6 @@ To enable auto redeployment you simply have to add the following annotation to t
|
|||||||
```yaml
|
```yaml
|
||||||
secrets.infisical.com/auto-reload: "true"
|
secrets.infisical.com/auto-reload: "true"
|
||||||
```
|
```
|
||||||
|
|
||||||
<Accordion title="Deployment example with auto redeploy enabled">
|
<Accordion title="Deployment example with auto redeploy enabled">
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
@@ -527,7 +526,11 @@ spec:
|
|||||||
- containerPort: 80
|
- containerPort: 80
|
||||||
```
|
```
|
||||||
</Accordion>
|
</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
|
## Global configuration
|
||||||
|
|
||||||
To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap.
|
To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap.
|
||||||
|
|||||||
+12
-31
@@ -32,10 +32,7 @@
|
|||||||
"thumbsRating": true
|
"thumbsRating": true
|
||||||
},
|
},
|
||||||
"api": {
|
"api": {
|
||||||
"baseUrl": [
|
"baseUrl": ["https://app.infisical.com", "http://localhost:8080"]
|
||||||
"https://app.infisical.com",
|
|
||||||
"http://localhost:8080"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"topbarLinks": [
|
"topbarLinks": [
|
||||||
{
|
{
|
||||||
@@ -76,9 +73,7 @@
|
|||||||
"documentation/getting-started/introduction",
|
"documentation/getting-started/introduction",
|
||||||
{
|
{
|
||||||
"group": "Quickstart",
|
"group": "Quickstart",
|
||||||
"pages": [
|
"pages": ["documentation/guides/local-development"]
|
||||||
"documentation/guides/local-development"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Guides",
|
"group": "Guides",
|
||||||
@@ -221,9 +216,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Reference architectures",
|
"group": "Reference architectures",
|
||||||
"pages": [
|
"pages": ["self-hosting/reference-architectures/aws-ecs"]
|
||||||
"self-hosting/reference-architectures/aws-ecs"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"self-hosting/ee",
|
"self-hosting/ee",
|
||||||
"self-hosting/faq"
|
"self-hosting/faq"
|
||||||
@@ -343,6 +336,7 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"integrations/cicd/circleci",
|
"integrations/cicd/circleci",
|
||||||
"integrations/cicd/travisci",
|
"integrations/cicd/travisci",
|
||||||
|
"integrations/cicd/rundeck",
|
||||||
"integrations/cicd/codefresh",
|
"integrations/cicd/codefresh",
|
||||||
"integrations/cloud/checkly"
|
"integrations/cloud/checkly"
|
||||||
]
|
]
|
||||||
@@ -379,21 +373,18 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Build Tool Integrations",
|
"group": "Build Tool Integrations",
|
||||||
"pages": [
|
"pages": ["integrations/build-tools/gradle"]
|
||||||
"integrations/build-tools/gradle"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "",
|
"group": "",
|
||||||
"pages": [
|
"pages": ["sdks/overview"]
|
||||||
"sdks/overview"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "SDK's",
|
"group": "SDK's",
|
||||||
"pages": [
|
"pages": [
|
||||||
"sdks/languages/node",
|
"sdks/languages/node",
|
||||||
"sdks/languages/python",
|
"sdks/languages/python",
|
||||||
|
"sdks/languages/go",
|
||||||
"sdks/languages/java",
|
"sdks/languages/java",
|
||||||
"sdks/languages/csharp"
|
"sdks/languages/csharp"
|
||||||
]
|
]
|
||||||
@@ -405,9 +396,7 @@
|
|||||||
"api-reference/overview/authentication",
|
"api-reference/overview/authentication",
|
||||||
{
|
{
|
||||||
"group": "Examples",
|
"group": "Examples",
|
||||||
"pages": [
|
"pages": ["api-reference/overview/examples/integration"]
|
||||||
"api-reference/overview/examples/integration"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -563,15 +552,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Service Tokens",
|
"group": "Service Tokens",
|
||||||
"pages": [
|
"pages": ["api-reference/endpoints/service-tokens/get"]
|
||||||
"api-reference/endpoints/service-tokens/get"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Audit Logs",
|
"group": "Audit Logs",
|
||||||
"pages": [
|
"pages": ["api-reference/endpoints/audit-logs/export-audit-log"]
|
||||||
"api-reference/endpoints/audit-logs/export-audit-log"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -587,9 +572,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "",
|
"group": "",
|
||||||
"pages": [
|
"pages": ["changelog/overview"]
|
||||||
"changelog/overview"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Contributing",
|
"group": "Contributing",
|
||||||
@@ -613,9 +596,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Contributing to SDK",
|
"group": "Contributing to SDK",
|
||||||
"pages": [
|
"pages": ["contributing/sdk/developing"]
|
||||||
"contributing/sdk/developing"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,438 @@
|
|||||||
|
---
|
||||||
|
title: "Infisical Go SDK"
|
||||||
|
sidebarTitle: "Go"
|
||||||
|
icon: "golang"
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
If you're working with Go Lang, the official [Infisical Go SDK](https://github.com/infisical/go-sdk) package is the easiest way to fetch and work with secrets for your application.
|
||||||
|
|
||||||
|
- [Package](https://pkg.go.dev/github.com/infisical/go-sdk)
|
||||||
|
- [Github Repository](https://github.com/infiscial/go-sdk)
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
infisical "github.com/infisical/go-sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
client, err := infisical.NewInfisicalClient(infisical.Config{
|
||||||
|
SiteUrl: "https://app.infisical.com", // Optional, default is https://app.infisical.com
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.Auth().UniversalAuthLogin("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Authentication failed: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKeySecret, err := client.Secrets().Retrieve(infisical.RetrieveSecretOptions{
|
||||||
|
SecretKey: "API_KEY",
|
||||||
|
Environment: "dev",
|
||||||
|
ProjectID: "YOUR_PROJECT_ID",
|
||||||
|
SecretPath: "/",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("API Key Secret: %v", apiKeySecret)
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This example demonstrates how to use the Infisical Go SDK in a simple Go application. The application retrieves a secret named `API_KEY` from the `dev` environment of the `YOUR_PROJECT_ID` project.
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ go get github.com/infisical/go-sdk
|
||||||
|
```
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
Import the SDK and create a client instance.
|
||||||
|
|
||||||
|
```go
|
||||||
|
client, err := infisical.NewInfisicalClient(infisical.Config{
|
||||||
|
SiteUrl: "https://app.infisical.com", // Optional, default is https://api.infisical.com
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ClientSettings methods
|
||||||
|
|
||||||
|
<ParamField query="options" type="object">
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="SiteUrl" type="string" optional>
|
||||||
|
The URL of the Infisical API. Default is `https://api.infisical.com`.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField query="UserAgent" type="string" required>
|
||||||
|
Optionally set the user agent that will be used for HTTP requests. _(Not recommended)_
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate.
|
||||||
|
|
||||||
|
#### Universal Auth
|
||||||
|
|
||||||
|
**Using environment variables**
|
||||||
|
|
||||||
|
Call `.Auth().UniversalAuthLogin()` with empty arguments to use the following environment variables:
|
||||||
|
|
||||||
|
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID.
|
||||||
|
- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret.
|
||||||
|
|
||||||
|
**Using the SDK directly**
|
||||||
|
```go
|
||||||
|
_, err := client.Auth().UniversalAuthLogin("CLIENT_ID", "CLIENT_SECRET")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GCP ID Token Auth
|
||||||
|
<Info>
|
||||||
|
Please note that this authentication method will only work if you're running your application on Google Cloud Platform.
|
||||||
|
Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
**Using environment variables**
|
||||||
|
|
||||||
|
Call `.Auth().GcpIdTokenAuthLogin()` with empty arguments to use the following environment variables:
|
||||||
|
|
||||||
|
- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||||
|
|
||||||
|
**Using the SDK directly**
|
||||||
|
```go
|
||||||
|
_, err := client.Auth().GcpIdTokenAuthLogin("YOUR_MACHINE_IDENTITY_ID")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GCP IAM Auth
|
||||||
|
|
||||||
|
**Using environment variables**
|
||||||
|
|
||||||
|
Call `.Auth().GcpIamAuthLogin()` with empty arguments to use the following environment variables:
|
||||||
|
|
||||||
|
- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||||
|
- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file.
|
||||||
|
|
||||||
|
**Using the SDK directly**
|
||||||
|
```go
|
||||||
|
_, err = client.Auth().GcpIamAuthLogin("MACHINE_IDENTITY_ID", "SERVICE_ACCOUNT_KEY_FILE_PATH")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### AWS IAM Auth
|
||||||
|
<Info>
|
||||||
|
Please note that this authentication method will only work if you're running your application on AWS.
|
||||||
|
Please [read more](/documentation/platform/identities/aws-auth) about this authentication method.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
**Using environment variables**
|
||||||
|
|
||||||
|
Call `.Auth().AwsIamAuthLogin()` with empty arguments to use the following environment variables:
|
||||||
|
|
||||||
|
- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||||
|
|
||||||
|
**Using the SDK directly**
|
||||||
|
```go
|
||||||
|
_, err = client.Auth().AwsIamAuthLogin("MACHINE_IDENTITY_ID")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Azure Auth
|
||||||
|
<Info>
|
||||||
|
Please note that this authentication method will only work if you're running your application on Azure.
|
||||||
|
Please [read more](/documentation/platform/identities/azure-auth) about this authentication method.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
**Using environment variables**
|
||||||
|
|
||||||
|
Call `.Auth().AzureAuthLogin()` with empty arguments to use the following environment variables:
|
||||||
|
|
||||||
|
- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||||
|
|
||||||
|
**Using the SDK directly**
|
||||||
|
```go
|
||||||
|
_, err = client.Auth().AzureAuthLogin("MACHINE_IDENTITY_ID")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Kubernetes Auth
|
||||||
|
<Info>
|
||||||
|
Please note that this authentication method will only work if you're running your application on Kubernetes.
|
||||||
|
Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
**Using environment variables**
|
||||||
|
|
||||||
|
Call `.Auth().KubernetesAuthLogin()` with empty arguments to use the following environment variables:
|
||||||
|
|
||||||
|
- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID.
|
||||||
|
- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`.
|
||||||
|
|
||||||
|
**Using the SDK directly**
|
||||||
|
```go
|
||||||
|
// Service account token path will default to /var/run/secrets/kubernetes.io/serviceaccount/token if empty value is passed
|
||||||
|
_, err = client.Auth().KubernetesAuthLogin("MACHINE_IDENTITY_ID", "SERVICE_ACCOUNT_TOKEN_PATH")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Working with Secrets
|
||||||
|
|
||||||
|
### client.Secrets().List(options)
|
||||||
|
|
||||||
|
```go
|
||||||
|
secrets, err := client.Secrets().List(infisical.ListSecretsOptions{
|
||||||
|
ProjectID: "PROJECT_ID",
|
||||||
|
Environment: "dev",
|
||||||
|
SecretPath: "/foo/bar",
|
||||||
|
AttachToProcessEnv: false,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Retrieve all secrets within the Infisical project and environment that client is connected to
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
<ParamField query="Parameters" type="object">
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="Environment" type="string" required>
|
||||||
|
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField query="ProjectID" type="string">
|
||||||
|
The project ID where the secret lives in.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField query="SecretPath" type="string" optional>
|
||||||
|
The path from where secrets should be fetched from.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField query="AttachToProcessEnv" type="boolean" default="false" optional>
|
||||||
|
Whether or not to set the fetched secrets to the process environment. If true, you can access the secrets like so `System.getenv("SECRET_NAME")`.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField query="IncludeImports" type="boolean" default="false" optional>
|
||||||
|
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField query="Recursive" type="boolean" default="false" optional>
|
||||||
|
Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField query="ExpandSecretReferences" type="boolean" default="true" optional>
|
||||||
|
Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference)
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
### client.Secrets().Get(options)
|
||||||
|
|
||||||
|
```go
|
||||||
|
secret, err := client.Secrets().Retrieve(infisical.RetrieveSecretOptions{
|
||||||
|
SecretKey: "API_KEY",
|
||||||
|
ProjectID: "PROJECT_ID",
|
||||||
|
Environment: "dev",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Retrieve a secret from Infisical.
|
||||||
|
|
||||||
|
By default, `Secrets().Get()` fetches and returns a shared secret.
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
<ParamField query="Parameters" type="object" optional>
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="SecretKey" type="string" required>
|
||||||
|
The key of the secret to retrieve.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="ProjectID" type="string" required>
|
||||||
|
The project ID where the secret lives in.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Environment" type="string" required>
|
||||||
|
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="SecretPath" type="string" optional>
|
||||||
|
The path from where secret should be fetched from.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Type" type="string" optional>
|
||||||
|
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
### client.Secrets().Create(options)
|
||||||
|
|
||||||
|
```go
|
||||||
|
secret, err := client.Secrets().Create(infisical.CreateSecretOptions{
|
||||||
|
ProjectID: "PROJECT_ID",
|
||||||
|
Environment: "dev",
|
||||||
|
|
||||||
|
SecretKey: "NEW_SECRET_KEY",
|
||||||
|
SecretValue: "NEW_SECRET_VALUE",
|
||||||
|
SecretComment: "This is a new secret",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a new secret in Infisical.
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
<ParamField query="Parameters" type="object" optional>
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="SecretKey" type="string" required>
|
||||||
|
The key of the secret to create.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="SecretValue" type="string" required>
|
||||||
|
The value of the secret.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="SecretComment" type="string" optional>
|
||||||
|
A comment for the secret.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="ProjectID" type="string" required>
|
||||||
|
The project ID where the secret lives in.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Environment" type="string" required>
|
||||||
|
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="SecretPath" type="string" optional>
|
||||||
|
The path from where secret should be created.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Type" type="string" optional>
|
||||||
|
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
### client.Secrets().Update(options)
|
||||||
|
|
||||||
|
```go
|
||||||
|
secret, err := client.Secrets().Update(infisical.UpdateSecretOptions{
|
||||||
|
ProjectID: "PROJECT_ID",
|
||||||
|
Environment: "dev",
|
||||||
|
SecretKey: "NEW_SECRET_KEY",
|
||||||
|
NewSecretValue: "NEW_SECRET_VALUE",
|
||||||
|
NewSkipMultilineEncoding: false,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Update an existing secret in Infisical.
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
<ParamField query="Parameters" type="object" optional>
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="SecretKey" type="string" required>
|
||||||
|
The key of the secret to update.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="NewSecretValue" type="string" required>
|
||||||
|
The new value of the secret.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="NewSkipMultilineEncoding" type="boolean" default="false" optional>
|
||||||
|
Whether or not to skip multiline encoding for the new secret value.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="ProjectID" type="string" required>
|
||||||
|
The project ID where the secret lives in.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Environment" type="string" required>
|
||||||
|
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="SecretPath" type="string" optional>
|
||||||
|
The path from where secret should be updated.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Type" type="string" optional>
|
||||||
|
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
### client.Secrets().Delete(options)
|
||||||
|
|
||||||
|
```go
|
||||||
|
secret, err := client.Secrets().Delete(infisical.DeleteSecretOptions{
|
||||||
|
ProjectID: "PROJECT_ID",
|
||||||
|
Environment: "dev",
|
||||||
|
SecretKey: "SECRET_KEY",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Delete a secret in Infisical.
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
<ParamField query="Parameters" type="object" optional>
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="SecretKey" type="string">
|
||||||
|
The key of the secret to update.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="ProjectID" type="string" required>
|
||||||
|
The project ID where the secret lives in.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Environment" type="string" required>
|
||||||
|
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="SecretPath" type="string" optional>
|
||||||
|
The path from where secret should be deleted.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Type" type="string" optional>
|
||||||
|
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
@@ -48,44 +48,44 @@ The platform utilizes Postgres to persist all of its data and Redis for caching
|
|||||||
Without email configuration, Infisical's core functions like sign-up/login and secret operations work, but this disables multi-factor authentication, email invites for projects, alerts for suspicious logins, and all other email-dependent features.
|
Without email configuration, Infisical's core functions like sign-up/login and secret operations work, but this disables multi-factor authentication, email invites for projects, alerts for suspicious logins, and all other email-dependent features.
|
||||||
|
|
||||||
<Accordion title="Generic Configuration">
|
<Accordion title="Generic Configuration">
|
||||||
<ParamField query="SMTP_HOST" type="string" default="none" optional>
|
<ParamField query="SMTP_HOST" type="string" default="none" optional>
|
||||||
Hostname to connect to for establishing SMTP connections
|
Hostname to connect to for establishing SMTP connections
|
||||||
</ParamField>
|
|
||||||
|
|
||||||
{" "}
|
|
||||||
|
|
||||||
<ParamField query="SMTP_USERNAME" type="string" default="none" optional>
|
|
||||||
Credential to connect to host (e.g. [email protected])
|
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
{" "}
|
|
||||||
|
|
||||||
<ParamField query="SMTP_PASSWORD" type="string" default="none" optional>
|
|
||||||
Credential to connect to host
|
|
||||||
</ParamField>
|
|
||||||
|
|
||||||
{" "}
|
|
||||||
|
|
||||||
<ParamField query="SMTP_PORT" type="string" default="587" optional>
|
<ParamField query="SMTP_PORT" type="string" default="587" optional>
|
||||||
Port to connect to for establishing SMTP connections
|
Port to connect to for establishing SMTP connections
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
{" "}
|
<ParamField query="SMTP_USERNAME" type="string" default="none" optional>
|
||||||
|
Credential to connect to host (e.g. [email protected])
|
||||||
<ParamField query="SMTP_SECURE" type="string" default="none" optional>
|
|
||||||
If true, use TLS when connecting to host. If false, TLS will be used if
|
|
||||||
STARTTLS is supported
|
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
{" "}
|
<ParamField query="SMTP_PASSWORD" type="string" default="none" optional>
|
||||||
|
Credential to connect to host
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
<ParamField query="SMTP_FROM_ADDRESS" type="string" default="none" optional>
|
<ParamField query="SMTP_FROM_ADDRESS" type="string" default="none" optional>
|
||||||
Email address to be used for sending emails
|
Email address to be used for sending emails
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField query="SMTP_FROM_NAME" type="string" default="none" optional>
|
<ParamField query="SMTP_FROM_NAME" type="string" default="none" optional>
|
||||||
Name label to be used in From field (e.g. Team)
|
Name label to be used in From field (e.g. Team)
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField query="SMTP_IGNORE_TLS" type="bool" default="false" optional>
|
||||||
|
If this is `true` and `SMTP_PORT` is not 465 then TLS is not used even if the
|
||||||
|
server supports STARTTLS extension.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField query="SMTP_REQUIRE_TLS" type="bool" default="true" optional>
|
||||||
|
If this is `true` and `SMTP_PORT` is not 465 then Infisical tries to use
|
||||||
|
STARTTLS even if the server does not advertise support for it. If the
|
||||||
|
connection can not be encrypted then message is not sent.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField query="SMTP_TLS_REJECT_UNAUTHORIZED" type="bool" default="true" optional>
|
||||||
|
If this is `true`, Infisical will validate the server's SSL/TLS certificate and reject the connection if the certificate is invalid or not trusted. If set to `false`, the client will accept the server's certificate regardless of its validity, which can be useful in development or testing environments but is not recommended for production use.
|
||||||
|
</ParamField>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="Twilio SendGrid">
|
<Accordion title="Twilio SendGrid">
|
||||||
@@ -105,7 +105,6 @@ SMTP_HOST=smtp.sendgrid.net
|
|||||||
SMTP_USERNAME=apikey
|
SMTP_USERNAME=apikey
|
||||||
SMTP_PASSWORD=SG.rqFsfjxYPiqE1lqZTgD_lz7x8IVLx # your SendGrid API Key from step above
|
SMTP_PASSWORD=SG.rqFsfjxYPiqE1lqZTgD_lz7x8IVLx # your SendGrid API Key from step above
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=true
|
|
||||||
[email protected] # your email address being used to send out emails
|
[email protected] # your email address being used to send out emails
|
||||||
SMTP_FROM_NAME=Infisical
|
SMTP_FROM_NAME=Infisical
|
||||||
```
|
```
|
||||||
@@ -128,7 +127,6 @@ SMTP_HOST=smtp.mailgun.org # obtained from credentials page
|
|||||||
[email protected] # obtained from credentials page
|
[email protected] # obtained from credentials page
|
||||||
SMTP_PASSWORD=password # obtained from credentials page
|
SMTP_PASSWORD=password # obtained from credentials page
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=true
|
|
||||||
[email protected] # your email address being used to send out emails
|
[email protected] # your email address being used to send out emails
|
||||||
SMTP_FROM_NAME=Infisical
|
SMTP_FROM_NAME=Infisical
|
||||||
```
|
```
|
||||||
@@ -159,7 +157,6 @@ SMTP_FROM_NAME=Infisical
|
|||||||
SMTP_USERNAME=xxx # your SMTP username
|
SMTP_USERNAME=xxx # your SMTP username
|
||||||
SMTP_PASSWORD=xxx # your SMTP password
|
SMTP_PASSWORD=xxx # your SMTP password
|
||||||
SMTP_PORT=465
|
SMTP_PORT=465
|
||||||
SMTP_SECURE=true
|
|
||||||
[email protected] # your email address being used to send out emails
|
[email protected] # your email address being used to send out emails
|
||||||
SMTP_FROM_NAME=Infisical
|
SMTP_FROM_NAME=Infisical
|
||||||
```
|
```
|
||||||
@@ -187,7 +184,6 @@ SMTP_HOST=smtp.socketlabs.com
|
|||||||
SMTP_USERNAME=username # obtained from your credentials
|
SMTP_USERNAME=username # obtained from your credentials
|
||||||
SMTP_PASSWORD=password # obtained from your credentials
|
SMTP_PASSWORD=password # obtained from your credentials
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=true
|
|
||||||
[email protected] # your email address being used to send out emails
|
[email protected] # your email address being used to send out emails
|
||||||
SMTP_FROM_NAME=Infisical
|
SMTP_FROM_NAME=Infisical
|
||||||
```
|
```
|
||||||
@@ -229,7 +225,6 @@ SMTP_HOST=smtp.resend.com
|
|||||||
SMTP_USERNAME=resend
|
SMTP_USERNAME=resend
|
||||||
SMTP_PASSWORD=YOUR_API_KEY
|
SMTP_PASSWORD=YOUR_API_KEY
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=true
|
|
||||||
[email protected] # your email address being used to send out emails
|
[email protected] # your email address being used to send out emails
|
||||||
SMTP_FROM_NAME=Infisical
|
SMTP_FROM_NAME=Infisical
|
||||||
```
|
```
|
||||||
@@ -253,7 +248,6 @@ SMTP_HOST=smtp.gmail.com
|
|||||||
[email protected] # your email
|
[email protected] # your email
|
||||||
SMTP_PASSWORD=password # your password
|
SMTP_PASSWORD=password # your password
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=true
|
|
||||||
[email protected]
|
[email protected]
|
||||||
SMTP_FROM_NAME=Infisical
|
SMTP_FROM_NAME=Infisical
|
||||||
```
|
```
|
||||||
@@ -277,7 +271,6 @@ SMTP_HOST=smtp.office365.com
|
|||||||
[email protected] # your username
|
[email protected] # your username
|
||||||
SMTP_PASSWORD=password # your password
|
SMTP_PASSWORD=password # your password
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=true
|
|
||||||
[email protected]
|
[email protected]
|
||||||
SMTP_FROM_NAME=Infisical
|
SMTP_FROM_NAME=Infisical
|
||||||
```
|
```
|
||||||
@@ -294,7 +287,6 @@ SMTP_HOST=smtp.zoho.com
|
|||||||
SMTP_USERNAME=username # your email
|
SMTP_USERNAME=username # your email
|
||||||
SMTP_PASSWORD=password # your password
|
SMTP_PASSWORD=password # your password
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_SECURE=true
|
|
||||||
[email protected] # your personal Zoho email or domain-based email linked to Zoho Mail
|
[email protected] # your personal Zoho email or domain-based email linked to Zoho Mail
|
||||||
SMTP_FROM_NAME=Infisical
|
SMTP_FROM_NAME=Infisical
|
||||||
```
|
```
|
||||||
@@ -318,6 +310,12 @@ SMTP_FROM_NAME=Infisical
|
|||||||
By default, users can only login via email/password based login method.
|
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.
|
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">
|
<Accordion title="Google">
|
||||||
Follow detailed guide to configure [Google SSO](/documentation/platform/sso/google)
|
Follow detailed guide to configure [Google SSO](/documentation/platform/sso/google)
|
||||||
|
|
||||||
@@ -369,11 +367,6 @@ To login into Infisical with OAuth providers such as Google, configure the assoc
|
|||||||
information.
|
information.
|
||||||
</Accordion>
|
</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
|
## 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.
|
To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box.
|
||||||
|
|||||||
+6
-1
@@ -2,6 +2,7 @@ ARG POSTHOG_HOST=https://app.posthog.com
|
|||||||
ARG POSTHOG_API_KEY=posthog-api-key
|
ARG POSTHOG_API_KEY=posthog-api-key
|
||||||
ARG INTERCOM_ID=intercom-id
|
ARG INTERCOM_ID=intercom-id
|
||||||
ARG NEXT_INFISICAL_PLATFORM_VERSION=next-infisical-platform-version
|
ARG NEXT_INFISICAL_PLATFORM_VERSION=next-infisical-platform-version
|
||||||
|
ARG CAPTCHA_SITE_KEY=captcha-site-key
|
||||||
|
|
||||||
FROM node:16-alpine AS deps
|
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.
|
# 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
|
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||||
ARG INTERCOM_ID
|
ARG INTERCOM_ID
|
||||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
||||||
|
ARG CAPTCHA_SITE_KEY
|
||||||
|
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
RUN npm run 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
|
BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
|
||||||
ARG NEXT_INFISICAL_PLATFORM_VERSION
|
ARG NEXT_INFISICAL_PLATFORM_VERSION
|
||||||
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION=$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 --chown=nextjs:nodejs --chmod=555 scripts ./scripts
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
RUN chown nextjs:nodejs ./public/data
|
RUN chown nextjs:nodejs ./public/data
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const ContentSecurityPolicy = `
|
const ContentSecurityPolicy = `
|
||||||
default-src 'self';
|
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';
|
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';
|
style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
|
||||||
child-src https://api.stripe.com;
|
child-src https://api.stripe.com;
|
||||||
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/;
|
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:*;
|
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:;
|
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;
|
media-src https://js.intercomcdn.com;
|
||||||
font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com;
|
font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com;
|
||||||
|
|||||||
Generated
+19
-1
@@ -4,7 +4,6 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "frontend",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
"@casl/react": "^3.1.0",
|
"@casl/react": "^3.1.0",
|
||||||
@@ -19,6 +18,7 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@hcaptcha/react-hcaptcha": "^1.10.1",
|
||||||
"@headlessui/react": "^1.7.7",
|
"@headlessui/react": "^1.7.7",
|
||||||
"@hookform/resolvers": "^2.9.10",
|
"@hookform/resolvers": "^2.9.10",
|
||||||
"@octokit/rest": "^19.0.7",
|
"@octokit/rest": "^19.0.7",
|
||||||
@@ -3200,6 +3200,24 @@
|
|||||||
"react": ">=16.3"
|
"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": {
|
"node_modules/@headlessui/react": {
|
||||||
"version": "1.7.18",
|
"version": "1.7.18",
|
||||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz",
|
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@hcaptcha/react-hcaptcha": "^1.10.1",
|
||||||
"@headlessui/react": "^1.7.7",
|
"@headlessui/react": "^1.7.7",
|
||||||
"@hookform/resolvers": "^2.9.10",
|
"@hookform/resolvers": "^2.9.10",
|
||||||
"@octokit/rest": "^19.0.7",
|
"@octokit/rest": "^19.0.7",
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ const integrationSlugNameMapping: Mapping = {
|
|||||||
northflank: "Northflank",
|
northflank: "Northflank",
|
||||||
windmill: "Windmill",
|
windmill: "Windmill",
|
||||||
"gcp-secret-manager": "GCP Secret Manager",
|
"gcp-secret-manager": "GCP Secret Manager",
|
||||||
"hasura-cloud": "Hasura Cloud"
|
"hasura-cloud": "Hasura Cloud",
|
||||||
|
rundeck: "Rundeck"
|
||||||
};
|
};
|
||||||
|
|
||||||
const envMapping: Mapping = {
|
const envMapping: Mapping = {
|
||||||
|
|||||||
@@ -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 |
@@ -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_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
|
if [ "$TELEMETRY_ENABLED" != "false" ]; then
|
||||||
echo "Telemetry is enabled"
|
echo "Telemetry is enabled"
|
||||||
|
|||||||
@@ -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_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
|
if [ "$TELEMETRY_ENABLED" != "false" ]; then
|
||||||
echo "Telemetry is enabled"
|
echo "Telemetry is enabled"
|
||||||
scripts/set-telemetry.sh true
|
scripts/set-telemetry.sh true
|
||||||
|
|||||||
@@ -30,11 +30,13 @@ export interface IsCliLoginSuccessful {
|
|||||||
const attemptLogin = async ({
|
const attemptLogin = async ({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
providerAuthToken
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
}: {
|
}: {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
|
captchaToken?: string;
|
||||||
}): Promise<IsCliLoginSuccessful> => {
|
}): Promise<IsCliLoginSuccessful> => {
|
||||||
const telemetry = new Telemetry().getInstance();
|
const telemetry = new Telemetry().getInstance();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -70,7 +72,8 @@ const attemptLogin = async ({
|
|||||||
} = await login2({
|
} = await login2({
|
||||||
email,
|
email,
|
||||||
clientProof,
|
clientProof,
|
||||||
providerAuthToken
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
});
|
});
|
||||||
if (mfaEnabled) {
|
if (mfaEnabled) {
|
||||||
// case: MFA is enabled
|
// case: MFA is enabled
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ interface IsLoginSuccessful {
|
|||||||
const attemptLogin = async ({
|
const attemptLogin = async ({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
providerAuthToken
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
}: {
|
}: {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
|
captchaToken?: string;
|
||||||
}): Promise<IsLoginSuccessful> => {
|
}): Promise<IsLoginSuccessful> => {
|
||||||
const telemetry = new Telemetry().getInstance();
|
const telemetry = new Telemetry().getInstance();
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
@@ -58,6 +60,7 @@ const attemptLogin = async ({
|
|||||||
iv,
|
iv,
|
||||||
tag
|
tag
|
||||||
} = await login2({
|
} = await login2({
|
||||||
|
captchaToken,
|
||||||
email,
|
email,
|
||||||
clientProof,
|
clientProof,
|
||||||
providerAuthToken
|
providerAuthToken
|
||||||
|
|||||||
@@ -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_API_KEY = process.env.NEXT_PUBLIC_POSTHOG_API_KEY!;
|
||||||
const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
|
const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
|
||||||
const INTERCOMid = process.env.NEXT_PUBLIC_INTERCOMid!;
|
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 };
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
className={twMerge(
|
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",
|
"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
|
dropdownContainerClassName
|
||||||
)}
|
)}
|
||||||
position={position}
|
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`,
|
outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
|
||||||
isSelected && "bg-primary",
|
isSelected && "bg-primary",
|
||||||
isDisabled &&
|
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
|
className
|
||||||
)}
|
)}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export type Login1DTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Login2DTO = {
|
export type Login2DTO = {
|
||||||
|
captchaToken?: string;
|
||||||
email: string;
|
email: string;
|
||||||
clientProof: string;
|
clientProof: string;
|
||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type IntegrationAuth = {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
algorithm: string;
|
algorithm: string;
|
||||||
keyEncoding: string;
|
keyEncoding: string;
|
||||||
|
url?: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const useCreateIntegration = () => {
|
|||||||
owner,
|
owner,
|
||||||
path,
|
path,
|
||||||
region,
|
region,
|
||||||
|
url,
|
||||||
scope,
|
scope,
|
||||||
secretPath,
|
secretPath,
|
||||||
metadata
|
metadata
|
||||||
@@ -56,6 +57,7 @@ export const useCreateIntegration = () => {
|
|||||||
targetService?: string;
|
targetService?: string;
|
||||||
targetServiceId?: string;
|
targetServiceId?: string;
|
||||||
owner?: string;
|
owner?: string;
|
||||||
|
url?: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
scope?: string;
|
scope?: string;
|
||||||
@@ -71,6 +73,9 @@ export const useCreateIntegration = () => {
|
|||||||
}[];
|
}[];
|
||||||
kmsKeyId?: string;
|
kmsKeyId?: string;
|
||||||
shouldDisableDelete?: boolean;
|
shouldDisableDelete?: boolean;
|
||||||
|
shouldMaskSecrets?: boolean;
|
||||||
|
shouldProtectSecrets?: boolean;
|
||||||
|
shouldEnableDelete?: boolean;
|
||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@@ -85,6 +90,7 @@ export const useCreateIntegration = () => {
|
|||||||
targetEnvironmentId,
|
targetEnvironmentId,
|
||||||
targetService,
|
targetService,
|
||||||
targetServiceId,
|
targetServiceId,
|
||||||
|
url,
|
||||||
owner,
|
owner,
|
||||||
path,
|
path,
|
||||||
scope,
|
scope,
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export type ServerStatus = {
|
|||||||
emailConfigured: boolean;
|
emailConfigured: boolean;
|
||||||
secretScanningConfigured: boolean;
|
secretScanningConfigured: boolean;
|
||||||
redisConfigured: boolean;
|
redisConfigured: boolean;
|
||||||
|
samlDefaultOrgSlug: boolean
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,15 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||||
import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api";
|
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 {
|
import {
|
||||||
useGetIntegrationAuthApps,
|
useGetIntegrationAuthApps,
|
||||||
useGetIntegrationAuthById
|
useGetIntegrationAuthById
|
||||||
@@ -34,6 +42,7 @@ export default function CloudflarePagesIntegrationPage() {
|
|||||||
const [targetApp, setTargetApp] = useState("");
|
const [targetApp, setTargetApp] = useState("");
|
||||||
const [targetAppId, setTargetAppId] = useState("");
|
const [targetAppId, setTargetAppId] = useState("");
|
||||||
const [targetEnvironment, setTargetEnvironment] = useState("");
|
const [targetEnvironment, setTargetEnvironment] = useState("");
|
||||||
|
const [shouldAutoRedeploy, setShouldAutoRedeploy] = useState(false);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
@@ -69,7 +78,10 @@ export default function CloudflarePagesIntegrationPage() {
|
|||||||
appId: targetAppId,
|
appId: targetAppId,
|
||||||
sourceEnvironment: selectedSourceEnvironment,
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
targetEnvironment,
|
targetEnvironment,
|
||||||
secretPath
|
secretPath,
|
||||||
|
metadata: {
|
||||||
|
shouldAutoRedeploy
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -169,6 +181,15 @@ export default function CloudflarePagesIntegrationPage() {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</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
|
<Button
|
||||||
onClick={handleButtonClick}
|
onClick={handleButtonClick}
|
||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
|
Switch,
|
||||||
Tab,
|
Tab,
|
||||||
TabList,
|
TabList,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
@@ -59,7 +60,7 @@ const schema = yup.object({
|
|||||||
selectedSourceEnvironment: yup.string().trim().required("Project Environment is required"),
|
selectedSourceEnvironment: yup.string().trim().required("Project Environment is required"),
|
||||||
secretPath: yup.string().trim().required("Secrets Path is required"),
|
secretPath: yup.string().trim().required("Secrets Path is required"),
|
||||||
secretSuffix: yup.string().trim().optional(),
|
secretSuffix: yup.string().trim().optional(),
|
||||||
|
shouldEnableDelete: yup.boolean().optional(),
|
||||||
scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(),
|
scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(),
|
||||||
|
|
||||||
repoIds: yup.mixed().when("scope", {
|
repoIds: yup.mixed().when("scope", {
|
||||||
@@ -98,7 +99,6 @@ type FormData = yup.InferType<typeof schema>;
|
|||||||
export default function GitHubCreateIntegrationPage() {
|
export default function GitHubCreateIntegrationPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutateAsync } = useCreateIntegration();
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
|
||||||
|
|
||||||
const integrationAuthId =
|
const integrationAuthId =
|
||||||
(queryString.parse(router.asPath.split("?")[1]).integrationAuthId as string) ?? "";
|
(queryString.parse(router.asPath.split("?")[1]).integrationAuthId as string) ?? "";
|
||||||
@@ -120,7 +120,8 @@ export default function GitHubCreateIntegrationPage() {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
secretPath: "/",
|
secretPath: "/",
|
||||||
scope: "github-repo",
|
scope: "github-repo",
|
||||||
repoIds: []
|
repoIds: [],
|
||||||
|
shouldEnableDelete: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -177,7 +178,8 @@ export default function GitHubCreateIntegrationPage() {
|
|||||||
app: targetApp.name, // repo name
|
app: targetApp.name, // repo name
|
||||||
owner: targetApp.owner, // repo owner
|
owner: targetApp.owner, // repo owner
|
||||||
metadata: {
|
metadata: {
|
||||||
secretSuffix: data.secretSuffix
|
secretSuffix: data.secretSuffix,
|
||||||
|
shouldEnableDelete: data.shouldEnableDelete
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -194,7 +196,8 @@ export default function GitHubCreateIntegrationPage() {
|
|||||||
scope: data.scope,
|
scope: data.scope,
|
||||||
owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name,
|
owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name,
|
||||||
metadata: {
|
metadata: {
|
||||||
secretSuffix: data.secretSuffix
|
secretSuffix: data.secretSuffix,
|
||||||
|
shouldEnableDelete: data.shouldEnableDelete
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@@ -211,7 +214,8 @@ export default function GitHubCreateIntegrationPage() {
|
|||||||
owner: repoOwner,
|
owner: repoOwner,
|
||||||
targetEnvironmentId: data.envId,
|
targetEnvironmentId: data.envId,
|
||||||
metadata: {
|
metadata: {
|
||||||
secretSuffix: data.secretSuffix
|
secretSuffix: data.secretSuffix,
|
||||||
|
shouldEnableDelete: data.shouldEnableDelete
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@@ -546,6 +550,21 @@ export default function GitHubCreateIntegrationPage() {
|
|||||||
animate={{ opacity: 1, translateX: 0 }}
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
exit={{ opacity: 0, translateX: 30 }}
|
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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="secretSuffix"
|
name="secretSuffix"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
|
Switch,
|
||||||
Tab,
|
Tab,
|
||||||
TabList,
|
TabList,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
@@ -58,7 +59,9 @@ const schema = yup.object({
|
|||||||
targetAppId: yup.string().required("GitLab project is required"),
|
targetAppId: yup.string().required("GitLab project is required"),
|
||||||
targetEnvironment: yup.string(),
|
targetEnvironment: yup.string(),
|
||||||
secretPrefix: yup.string(),
|
secretPrefix: yup.string(),
|
||||||
secretSuffix: yup.string()
|
secretSuffix: yup.string(),
|
||||||
|
shouldMaskSecrets: yup.boolean(),
|
||||||
|
shouldProtectSecrets: yup.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = yup.InferType<typeof schema>;
|
type FormData = yup.InferType<typeof schema>;
|
||||||
@@ -138,7 +141,9 @@ export default function GitLabCreateIntegrationPage() {
|
|||||||
targetAppId,
|
targetAppId,
|
||||||
targetEnvironment,
|
targetEnvironment,
|
||||||
secretPrefix,
|
secretPrefix,
|
||||||
secretSuffix
|
secretSuffix,
|
||||||
|
shouldMaskSecrets,
|
||||||
|
shouldProtectSecrets
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -156,7 +161,9 @@ export default function GitLabCreateIntegrationPage() {
|
|||||||
secretPath,
|
secretPath,
|
||||||
metadata: {
|
metadata: {
|
||||||
secretPrefix,
|
secretPrefix,
|
||||||
secretSuffix
|
secretSuffix,
|
||||||
|
shouldMaskSecrets,
|
||||||
|
shouldProtectSecrets
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -390,6 +397,36 @@ export default function GitLabCreateIntegrationPage() {
|
|||||||
exit={{ opacity: 0, translateX: 30 }}
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
className="pb-[14.25rem]"
|
className="pb-[14.25rem]"
|
||||||
>
|
>
|
||||||
|
<div className="ml-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="shouldMaskSecrets"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Switch
|
||||||
|
id="should-mask-secrets"
|
||||||
|
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||||
|
isChecked={value}
|
||||||
|
>
|
||||||
|
<div className="max-w-md">Mark Infisical secrets in Gitlab as 'Masked' secrets</div>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-1 mt-4 mb-5">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="shouldProtectSecrets"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Switch
|
||||||
|
id="should-protect-secrets"
|
||||||
|
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||||
|
isChecked={value}
|
||||||
|
>
|
||||||
|
Mark Infisical secrets in Gitlab as 'Protected' secrets
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="secretPrefix"
|
name="secretPrefix"
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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:[email protected]"
|
||||||
|
>
|
||||||
|
support@infisical.com
|
||||||
|
</a>{" "}
|
||||||
|
if the issue persists.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
RundeckCreateIntegrationPage.requireAuth = true;
|
||||||
@@ -128,6 +128,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
|
|||||||
case "hasura-cloud":
|
case "hasura-cloud":
|
||||||
link = `${window.location.origin}/integrations/hasura-cloud/authorize`;
|
link = `${window.location.origin}/integrations/hasura-cloud/authorize`;
|
||||||
break;
|
break;
|
||||||
|
case "rundeck":
|
||||||
|
link = `${window.location.origin}/integrations/rundeck/authorize`;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -141,7 +141,8 @@ export const IntegrationsSection = ({
|
|||||||
label={
|
label={
|
||||||
(integration.integration === "qovery" && integration?.scope) ||
|
(integration.integration === "qovery" && integration?.scope) ||
|
||||||
(integration.integration === "aws-secret-manager" && "Secret") ||
|
(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?.integration === "terraform-cloud" && "Project") ||
|
||||||
(integration?.scope === "github-org" && "Organization") ||
|
(integration?.scope === "github-org" && "Organization") ||
|
||||||
(["github-repo", "github-env"].includes(integration?.scope as string) &&
|
(["github-repo", "github-env"].includes(integration?.scope as string) &&
|
||||||
@@ -153,7 +154,7 @@ export const IntegrationsSection = ({
|
|||||||
{(integration.integration === "hashicorp-vault" &&
|
{(integration.integration === "hashicorp-vault" &&
|
||||||
`${integration.app} - path: ${integration.path}`) ||
|
`${integration.app} - path: ${integration.path}`) ||
|
||||||
(integration.scope === "github-org" && `${integration.owner}`) ||
|
(integration.scope === "github-org" && `${integration.owner}`) ||
|
||||||
(integration.integration === "aws-parameter-store" &&
|
(["aws-parameter-store", "rundeck"].includes(integration.integration) &&
|
||||||
`${integration.path}`) ||
|
`${integration.path}`) ||
|
||||||
(integration.scope?.startsWith("github-") &&
|
(integration.scope?.startsWith("github-") &&
|
||||||
`${integration.owner}/${integration.app}`) ||
|
`${integration.owner}/${integration.app}`) ||
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { FormEvent, useEffect, useState } from "react";
|
import { FormEvent, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
|
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||||
|
|
||||||
import Error from "@app/components/basic/Error";
|
import Error from "@app/components/basic/Error";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||||
|
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
|
||||||
import { Button, Input } from "@app/components/v2";
|
import { Button, Input } from "@app/components/v2";
|
||||||
import { useServerConfig } from "@app/context";
|
import { useServerConfig } from "@app/context";
|
||||||
|
import { useFetchServerStatus } from "@app/hooks/api";
|
||||||
|
|
||||||
import { navigateUserToSelectOrg } from "../../Login.utils";
|
import { navigateUserToSelectOrg } from "../../Login.utils";
|
||||||
|
|
||||||
@@ -31,21 +34,18 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
const [loginError, setLoginError] = useState(false);
|
const [loginError, setLoginError] = useState(false);
|
||||||
const { config } = useServerConfig();
|
const { config } = useServerConfig();
|
||||||
const queryParams = new URLSearchParams(window.location.search);
|
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(() => {
|
useEffect(() => {
|
||||||
if (
|
if (serverDetails?.samlDefaultOrgSlug){
|
||||||
process.env.NEXT_PUBLIC_SAML_ORG_SLUG &&
|
const callbackPort = queryParams.get("callback_port");
|
||||||
process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default"
|
const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${serverDetails?.samlDefaultOrgSlug}${callbackPort ? `?callback_port=${callbackPort}` : ""}`
|
||||||
) {
|
router.push(redirectUrl);
|
||||||
const callbackPort = queryParams.get("callback_port");
|
}
|
||||||
window.open(
|
}, [serverDetails?.samlDefaultOrgSlug]);
|
||||||
`/api/v1/sso/redirect/saml2/organizations/${process.env.NEXT_PUBLIC_SAML_ORG_SLUG}${
|
|
||||||
callbackPort ? `?callback_port=${callbackPort}` : ""
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
|
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -61,7 +61,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
// attemptCliLogin
|
// attemptCliLogin
|
||||||
const isCliLoginSuccessful = await attemptCliLogin({
|
const isCliLoginSuccessful = await attemptCliLogin({
|
||||||
email: email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
password
|
password,
|
||||||
|
captchaToken
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
||||||
@@ -83,7 +84,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
} else {
|
} else {
|
||||||
const isLoginSuccessful = await attemptLogin({
|
const isLoginSuccessful = await attemptLogin({
|
||||||
email: email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
password
|
password,
|
||||||
|
captchaToken
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoginSuccessful && isLoginSuccessful.success) {
|
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||||
@@ -117,6 +119,12 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (err.response.data.error === "Captcha Required") {
|
||||||
|
setShouldShowCaptcha(true);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoginError(true);
|
setLoginError(true);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Login unsuccessful. Double-check your credentials and try again.",
|
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);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -245,8 +258,19 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
className="select:-webkit-autofill:focus h-10"
|
className="select:-webkit-autofill:focus h-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<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
|
<Button
|
||||||
|
disabled={shouldShowCaptcha && captchaToken === ""}
|
||||||
type="submit"
|
type="submit"
|
||||||
size="sm"
|
size="sm"
|
||||||
isFullWidth
|
isFullWidth
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import jwt_decode from "jwt-decode";
|
import jwt_decode from "jwt-decode";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||||
|
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
|
||||||
import { Button, Input } from "@app/components/v2";
|
import { Button, Input } from "@app/components/v2";
|
||||||
import { useUpdateUserAuthMethods } from "@app/hooks/api";
|
import { useUpdateUserAuthMethods } from "@app/hooks/api";
|
||||||
import { useSelectOrganization } from "@app/hooks/api/auth/queries";
|
import { useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||||
@@ -41,6 +43,10 @@ export const PasswordStep = ({
|
|||||||
providerAuthToken
|
providerAuthToken
|
||||||
) as any;
|
) as any;
|
||||||
|
|
||||||
|
const [captchaToken, setCaptchaToken] = useState("");
|
||||||
|
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
|
||||||
|
const captchaRef = useRef<HCaptcha>(null);
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
@@ -51,7 +57,8 @@ export const PasswordStep = ({
|
|||||||
const isCliLoginSuccessful = await attemptCliLogin({
|
const isCliLoginSuccessful = await attemptCliLogin({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
providerAuthToken
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
||||||
@@ -99,7 +106,8 @@ export const PasswordStep = ({
|
|||||||
const loginAttempt = await attemptLogin({
|
const loginAttempt = await attemptLogin({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
providerAuthToken
|
providerAuthToken,
|
||||||
|
captchaToken
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loginAttempt && loginAttempt.success) {
|
if (loginAttempt && loginAttempt.success) {
|
||||||
@@ -158,11 +166,21 @@ export const PasswordStep = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (err.response.data.error === "Captcha Required") {
|
||||||
|
setShouldShowCaptcha(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Login unsuccessful. Double-check your master password and try again.",
|
text: "Login unsuccessful. Double-check your master password and try again.",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (captchaRef.current) {
|
||||||
|
captchaRef.current.resetCaptcha();
|
||||||
|
}
|
||||||
|
setCaptchaToken("");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -194,8 +212,19 @@ export const PasswordStep = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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
|
<Button
|
||||||
|
disabled={shouldShowCaptcha && captchaToken === ""}
|
||||||
type="submit"
|
type="submit"
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
|
|||||||
+28
-10
@@ -45,7 +45,6 @@ export const computeImportedSecretRows = (
|
|||||||
if (importedSecIndex === -1) return [];
|
if (importedSecIndex === -1) return [];
|
||||||
|
|
||||||
const importedSec = importSecrets[importedSecIndex];
|
const importedSec = importSecrets[importedSecIndex];
|
||||||
|
|
||||||
const overridenSec: Record<string, { env: string; secretPath: string }> = {};
|
const overridenSec: Record<string, { env: string; secretPath: string }> = {};
|
||||||
|
|
||||||
for (let i = importedSecIndex + 1; i < importSecrets.length; i += 1) {
|
for (let i = importedSecIndex + 1; i < importSecrets.length; i += 1) {
|
||||||
@@ -61,11 +60,28 @@ export const computeImportedSecretRows = (
|
|||||||
overridenSec[el.key] = { env: SECRET_IN_DASHBOARD, secretPath: "" };
|
overridenSec[el.key] = { env: SECRET_IN_DASHBOARD, secretPath: "" };
|
||||||
});
|
});
|
||||||
|
|
||||||
return importedSec.secrets.map(({ key, value }) => ({
|
const importedEntry: Record<string, boolean> = {};
|
||||||
key,
|
const importedSecretEntries: {
|
||||||
value,
|
key: string;
|
||||||
overriden: overridenSec?.[key]
|
value: string;
|
||||||
}));
|
overriden: {
|
||||||
|
env: string;
|
||||||
|
secretPath: string;
|
||||||
|
};
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
importedSec.secrets.forEach(({ key, value }) => {
|
||||||
|
if (!importedEntry[key]) {
|
||||||
|
importedSecretEntries.push({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
overriden: overridenSec?.[key]
|
||||||
|
});
|
||||||
|
importedEntry[key] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return importedSecretEntries;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -159,8 +175,9 @@ export const SecretImportListView = ({
|
|||||||
importEnv.slug === environment &&
|
importEnv.slug === environment &&
|
||||||
isReserved &&
|
isReserved &&
|
||||||
importPath ===
|
importPath ===
|
||||||
`${secretPath === "/" ? "" : secretPath}/${ReservedFolders.SecretReplication
|
`${secretPath === "/" ? "" : secretPath}/${
|
||||||
}${replicationImportId}`
|
ReservedFolders.SecretReplication
|
||||||
|
}${replicationImportId}`
|
||||||
);
|
);
|
||||||
if (reservedImport) {
|
if (reservedImport) {
|
||||||
setReplicationSecrets((state) => ({
|
setReplicationSecrets((state) => ({
|
||||||
@@ -206,8 +223,9 @@ export const SecretImportListView = ({
|
|||||||
isOpen={popUp.deleteSecretImport.isOpen}
|
isOpen={popUp.deleteSecretImport.isOpen}
|
||||||
deleteKey="unlink"
|
deleteKey="unlink"
|
||||||
title="Do you want to remove this secret import?"
|
title="Do you want to remove this secret import?"
|
||||||
subTitle={`This will unlink secrets from environment ${(popUp.deleteSecretImport?.data as TSecretImport)?.importEnv
|
subTitle={`This will unlink secrets from environment ${
|
||||||
} of path ${(popUp.deleteSecretImport?.data as TSecretImport)?.importPath}?`}
|
(popUp.deleteSecretImport?.data as TSecretImport)?.importEnv
|
||||||
|
} of path ${(popUp.deleteSecretImport?.data as TSecretImport)?.importPath}?`}
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
|
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
|
||||||
onDeleteApproved={handleSecretImportDelete}
|
onDeleteApproved={handleSecretImportDelete}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -393,15 +393,15 @@ export const SecretDetailSidebar = ({
|
|||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Switch
|
<Switch
|
||||||
id="skipmultiencoding-option"
|
id="skipmultiencoding-option"
|
||||||
onCheckedChange={(isChecked) => onChange(!isChecked)}
|
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||||
isChecked={!value}
|
isChecked={value}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
className="items-center"
|
className="items-center"
|
||||||
>
|
>
|
||||||
Enable multi line encoding
|
Multi line encoding
|
||||||
<Tooltip
|
<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]"
|
className="z-[100]"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
|
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
|
||||||
|
|||||||
@@ -454,12 +454,12 @@ export const SecretOverviewPage = () => {
|
|||||||
const filteredSecretNames = secKeys
|
const filteredSecretNames = secKeys
|
||||||
?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase()))
|
?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase()))
|
||||||
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
||||||
const filteredFolderNames = folderNames?.filter((name) =>
|
const filteredFolderNames = folderNames
|
||||||
name.toLowerCase().includes(searchFilter.toLowerCase())
|
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
|
||||||
);
|
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
||||||
const filteredDynamicSecrets = dynamicSecretNames?.filter((name) =>
|
const filteredDynamicSecrets = dynamicSecretNames
|
||||||
name.toLowerCase().includes(searchFilter.toLowerCase())
|
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
|
||||||
);
|
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
||||||
|
|
||||||
const isTableEmpty =
|
const isTableEmpty =
|
||||||
!(
|
!(
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<SecretInput
|
<SecretInput
|
||||||
isVisible
|
isVisible={false}
|
||||||
{...field}
|
{...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]"
|
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]"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -232,7 +232,6 @@ func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context
|
|||||||
}
|
}
|
||||||
|
|
||||||
managedKubeSecret.Data = plainProcessedSecrets
|
managedKubeSecret.Data = plainProcessedSecrets
|
||||||
managedKubeSecret.ObjectMeta.Annotations = map[string]string{}
|
|
||||||
managedKubeSecret.ObjectMeta.Annotations[SECRET_VERSION_ANNOTATION] = ETag
|
managedKubeSecret.ObjectMeta.Annotations[SECRET_VERSION_ANNOTATION] = ETag
|
||||||
|
|
||||||
err := r.Client.Update(ctx, &managedKubeSecret)
|
err := r.Client.Update(ctx, &managedKubeSecret)
|
||||||
|
|||||||
Reference in New Issue
Block a user