Merge remote-tracking branch 'origin/main' into misc/metrics-observability

This commit is contained in:
Sheen Capadngan
2024-06-11 23:50:08 +08:00
89 changed files with 2004 additions and 312 deletions
+4
View File
@@ -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
+6 -6
View File
@@ -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 /
+2 -2
View File
@@ -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`
+6 -2
View File
@@ -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 () => {}
}; };
}; };
+11
View File
@@ -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}});
+20 -1
View File
@@ -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);
}
@@ -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");
}
});
}
+4
View File
@@ -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>>;
+26
View File
@@ -0,0 +1,26 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const KmsKeysSchema = z.object({
id: z.string().uuid(),
encryptedKey: zodBuffer,
encryptionAlgorithm: z.string(),
version: z.number().default(1),
description: z.string().nullable().optional(),
isDisabled: z.boolean().default(false).nullable().optional(),
isReserved: z.boolean().default(true).nullable().optional(),
projectId: z.string().nullable().optional(),
orgId: z.string().uuid().nullable().optional()
});
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
export type TKmsKeysInsert = Omit<z.input<typeof KmsKeysSchema>, TImmutableDBKeys>;
export type TKmsKeysUpdate = Partial<Omit<z.input<typeof KmsKeysSchema>, TImmutableDBKeys>>;
+19
View File
@@ -0,0 +1,19 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const KmsRootConfigSchema = z.object({
id: z.string().uuid(),
encryptedRootKey: zodBuffer
});
export type TKmsRootConfig = z.infer<typeof KmsRootConfigSchema>;
export type TKmsRootConfigInsert = Omit<z.input<typeof KmsRootConfigSchema>, TImmutableDBKeys>;
export type TKmsRootConfigUpdate = Partial<Omit<z.input<typeof KmsRootConfigSchema>, TImmutableDBKeys>>;
+5 -1
View File
@@ -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";
+2 -1
View File
@@ -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(
+1 -1
View File
@@ -391,7 +391,7 @@ export const scimServiceFactory = ({
); );
} }
} }
await licenseService.updateSubscriptionOrgMemberCount(org.id);
return { user, orgMembership }; return { user, orgMembership };
}); });
+34 -1
View File
@@ -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
}; };
}; };
+7 -1
View File
@@ -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: {
+24 -12
View File
@@ -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
}
};
};
+49
View File
@@ -0,0 +1,49 @@
import crypto from "crypto";
import { SymmetricEncryption, TSymmetricEncryptionFns } from "./types";
const getIvLength = () => {
return 12;
};
const getTagLength = () => {
return 16;
};
export const symmetricCipherService = (type: SymmetricEncryption): TSymmetricEncryptionFns => {
const IV_LENGTH = getIvLength();
const TAG_LENGTH = getTagLength();
const encrypt = (text: Buffer, key: Buffer) => {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(type, key, iv);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
// Get the authentication tag
const tag = cipher.getAuthTag();
// Concatenate IV, encrypted text, and tag into a single buffer
const ciphertextBlob = Buffer.concat([iv, encrypted, tag]);
return ciphertextBlob;
};
const decrypt = (ciphertextBlob: Buffer, key: Buffer) => {
// Extract the IV, encrypted text, and tag from the buffer
const iv = ciphertextBlob.subarray(0, IV_LENGTH);
const tag = ciphertextBlob.subarray(-TAG_LENGTH);
const encrypted = ciphertextBlob.subarray(IV_LENGTH, -TAG_LENGTH);
const decipher = crypto.createDecipheriv(type, key, iv);
decipher.setAuthTag(tag);
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
return decrypted;
};
return {
encrypt,
decrypt
};
};
+2
View File
@@ -0,0 +1,2 @@
export { symmetricCipherService } from "./cipher";
export { SymmetricEncryption } from "./types";
+9
View File
@@ -0,0 +1,9 @@
export enum SymmetricEncryption {
AES_GCM_256 = "aes-256-gcm",
AES_GCM_128 = "aes-128-gcm"
}
export type TSymmetricEncryptionFns = {
encrypt: (text: Buffer, key: Buffer) => Buffer;
decrypt: (blob: Buffer, key: Buffer) => Buffer;
};
+2
View File
@@ -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;
+2 -1
View File
@@ -9,7 +9,8 @@ export {
encryptAsymmetric, encryptAsymmetric,
encryptSymmetric, encryptSymmetric,
encryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8,
generateAsymmetricKeyPair generateAsymmetricKeyPair,
randomSecureBytes
} from "./encryption"; } from "./encryption";
export { export {
decryptIntegrationAuths, decryptIntegrationAuths,
+4
View File
@@ -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)"
});
+1 -2
View File
@@ -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");
+17 -2
View File
@@ -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({
+3 -1
View File
@@ -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">;
+10
View File
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TKmsDALFactory = ReturnType<typeof kmsDALFactory>;
export const kmsDALFactory = (db: TDbClient) => {
const kmsOrm = ormify(db, TableName.KmsKey);
return kmsOrm;
};
@@ -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;
};
+126
View File
@@ -0,0 +1,126 @@
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { randomSecureBytes } from "@app/lib/crypto";
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TKmsDALFactory } from "./kms-dal";
import { TKmsRootConfigDALFactory } from "./kms-root-config-dal";
import { TDecryptWithKmsDTO, TEncryptWithKmsDTO, TGenerateKMSDTO } from "./kms-types";
type TKmsServiceFactoryDep = {
kmsDAL: TKmsDALFactory;
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
};
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
const KMS_ROOT_CREATION_WAIT_TIME = 10;
// akhilmhdh: Don't edit this value. This is measured for blob concatination in kms
const KMS_VERSION = "v01";
const KMS_VERSION_BLOB_LENGTH = 3;
export const kmsServiceFactory = ({ kmsDAL, kmsRootConfigDAL, keyStore }: TKmsServiceFactoryDep) => {
let ROOT_ENCRYPTION_KEY = Buffer.alloc(0);
// this is used symmetric encryption
const generateKmsKey = async ({ scopeId, scopeType, isReserved = true }: TGenerateKMSDTO) => {
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKeyMaterial = randomSecureBytes(32);
const encryptedKeyMaterial = cipher.encrypt(kmsKeyMaterial, ROOT_ENCRYPTION_KEY);
const { encryptedKey, ...doc } = await kmsDAL.create({
version: 1,
encryptedKey: encryptedKeyMaterial,
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
isReserved,
orgId: scopeType === "org" ? scopeId : undefined,
projectId: scopeType === "project" ? scopeId : undefined
});
return doc;
};
const encrypt = async ({ kmsId, plainText }: TEncryptWithKmsDTO) => {
const kmsDoc = await kmsDAL.findById(kmsId);
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" });
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY);
const encryptedPlainTextBlob = cipher.encrypt(plainText, kmsKey);
// Buffer#1 encrypted text + Buffer#2 version number
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
return { cipherTextBlob };
};
const decrypt = async ({ cipherTextBlob: versionedCipherTextBlob, kmsId }: TDecryptWithKmsDTO) => {
const kmsDoc = await kmsDAL.findById(kmsId);
if (!kmsDoc) throw new BadRequestError({ message: "KMS ID not found" });
// akhilmhdh: as more encryption are added do a check here on kmsDoc.encryptionAlgorithm
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
const kmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY);
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
const decryptedBlob = cipher.decrypt(cipherTextBlob, kmsKey);
return decryptedBlob;
};
const startService = async () => {
const appCfg = getConfig();
// This will switch to a seal process and HMS flow in future
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
// if root key its base64 encoded
const isBase64 = !appCfg.ENCRYPTION_KEY;
if (!encryptionKey) throw new Error("Root encryption key not found for KMS service.");
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null);
if (!lock) {
await keyStore.waitTillReady({
key: KMS_ROOT_CREATION_WAIT_KEY,
keyCheckCb: (val) => val === "true",
waitingCb: () => logger.info("KMS. Waiting for leader to finish creation of KMS Root Key")
});
}
// check if KMS root key was already generated and saved in DB
const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
if (kmsRootConfig) {
if (lock) await lock.release();
logger.info("KMS: Encrypted ROOT Key found from DB. Decrypting.");
const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
// set the flag so that other instancen nodes can start
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
logger.info("KMS: Loading ROOT Key into Memory.");
ROOT_ENCRYPTION_KEY = decryptedRootKey;
return;
}
logger.info("KMS: Generating ROOT Key");
const newRootKey = randomSecureBytes(32);
const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer);
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
await kmsRootConfigDAL.create({ encryptedRootKey, id: KMS_ROOT_CONFIG_UUID });
// set the flag so that other instancen nodes can start
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
logger.info("KMS: Saved and loaded ROOT Key into memory");
if (lock) await lock.release();
ROOT_ENCRYPTION_KEY = newRootKey;
};
return {
startService,
generateKmsKey,
encrypt,
decrypt
};
};
+15
View File
@@ -0,0 +1,15 @@
export type TGenerateKMSDTO = {
scopeType: "project" | "org";
scopeId: string;
isReserved?: boolean;
};
export type TEncryptWithKmsDTO = {
kmsId: string;
plainText: Buffer;
};
export type TDecryptWithKmsDTO = {
kmsId: string;
cipherTextBlob: Buffer;
};
+1
View File
@@ -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,
+6 -5
View File
@@ -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
}; };
}; };
+11 -3
View File
@@ -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
}); });
} }
+63 -11
View File
@@ -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
}; };
}; };
+1 -14
View File
@@ -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

+39
View File
@@ -0,0 +1,39 @@
---
title: "Rundeck"
description: "How to sync secrets from Infisical to Rundeck"
---
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
<Steps>
<Step title="Authorize Infisical for Rundeck">
Obtain a User API Token in the Profile settings of Rundeck
![integrations rundeck token](../../images/integrations/rundeck/integrations-rundeck-token.png)
Navigate to your project's integrations tab in Infisical.
![integrations](../../images/integrations.png)
Press on the Rundeck tile and input your Rundeck instance Base URL and User API token to grant Infisical access to manage Rundeck keys
![integrations rundeck authorization](../../images/integrations/rundeck/integrations-rundeck-auth.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant
Infisical access to your project's environment variables. Although this step
breaks E2EE, it's necessary for Infisical to sync the environment variables to
the cloud platform.
</Info>
</Step>
<Step title="Start integration">
Select which Infisical environment secrets you want to sync to a Rundeck Key Storage Path and press create integration to start syncing secrets to Rundeck.
![create integration rundeck](../../images/integrations/rundeck/integrations-rundeck-create.png)
![integrations rundeck](../../images/integrations/rundeck/integrations-rundeck.png)
</Step>
</Steps>
+5 -4
View File
@@ -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 |
+5 -2
View File
@@ -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
View File
@@ -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"
]
} }
] ]
} }
+438
View File
@@ -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>
+31 -38
View File
@@ -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
View File
@@ -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
+4 -5
View File
@@ -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;
+19 -1
View File
@@ -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",
+1
View File
@@ -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",
+2 -1
View File
@@ -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"
+2
View File
@@ -6,6 +6,8 @@ scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTER
scripts/replace-variable.sh "$BAKED_NEXT_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG" scripts/replace-variable.sh "$BAKED_NEXT_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 };
+2 -1
View File
@@ -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}
+1
View File
@@ -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 &apos;Masked&apos; 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 &apos;Protected&apos; 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;
} }
@@ -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"
@@ -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)