mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-02 08:27:38 +00:00
Compare commits
67 Commits
feat/added
...
fix/resolv
Author | SHA1 | Date | |
---|---|---|---|
|
05ed00834a | ||
|
38b0edf510 | ||
|
56b9506b39 | ||
|
ae34e015db | ||
|
7c42768cd8 | ||
|
c8a3252c1a | ||
|
0bba1801b9 | ||
|
a61e92c49c | ||
|
9945d249d6 | ||
|
b31d2be3f3 | ||
|
8329cbf299 | ||
|
9138ab8ed7 | ||
|
ea517bc199 | ||
|
a82b813553 | ||
|
af03f706ba | ||
|
9cf5bbc5d5 | ||
|
9161dd5e13 | ||
|
aafddaa856 | ||
|
776f464bee | ||
|
104b0d6c60 | ||
|
9303124f5f | ||
|
03c9a5606b | ||
|
f4a1a00b59 | ||
|
b9933d711c | ||
|
1abdb531d9 | ||
|
59b3123eb3 | ||
|
c1954a6386 | ||
|
0bbb86ee2a | ||
|
7c9c65312b | ||
|
8a46cbd08f | ||
|
fa05639592 | ||
|
429b2a284d | ||
|
6c596092b0 | ||
|
fcd13eac8a | ||
|
1fb653754c | ||
|
bb1d73b0f5 | ||
|
59e9226d85 | ||
|
e6f42e1231 | ||
|
06e7a90a44 | ||
|
f075ff23a9 | ||
|
6d40d951c6 | ||
|
e5b7ebbabf | ||
|
610dd07a57 | ||
|
9d6d7540dc | ||
|
847c2c67ec | ||
|
faa1572faf | ||
|
d288bcbd74 | ||
|
af1d30a49a | ||
|
2bd9ad0137 | ||
|
76a424dcfb | ||
|
9d46c269d4 | ||
|
cd92ce627c | ||
|
15c05b4910 | ||
|
65d88ef08e | ||
|
81e4129e51 | ||
|
c1ca2a6f8c | ||
|
9b6602a8e9 | ||
|
22db286dda | ||
|
62f92b0bfa | ||
|
abbef4fc44 | ||
|
34ca942f9d | ||
|
1acf25dd53 | ||
|
a0653883b6 | ||
|
f3a4c32e3a | ||
|
4c06f134fb | ||
|
c34c13887a | ||
|
3e9ce79398 |
@@ -63,3 +63,7 @@ CLIENT_SECRET_GITHUB_LOGIN=
|
||||
|
||||
CLIENT_ID_GITLAB_LOGIN=
|
||||
CLIENT_SECRET_GITLAB_LOGIN=
|
||||
|
||||
CAPTCHA_SECRET=
|
||||
|
||||
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
||||
|
@@ -40,13 +40,14 @@ jobs:
|
||||
REDIS_URL: redis://172.17.0.1:6379
|
||||
DB_CONNECTION_URI: postgres://infisical:infisical@172.17.0.1:5432/infisical?sslmode=disable
|
||||
JWT_AUTH_SECRET: something-random
|
||||
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21.5'
|
||||
- name: Wait for container to be stable and check logs
|
||||
run: |
|
||||
SECONDS=0
|
||||
HEALTHY=0
|
||||
r HEALTHY=0
|
||||
while [ $SECONDS -lt 60 ]; do
|
||||
if docker ps | grep infisical-api | grep -q healthy; then
|
||||
echo "Container is healthy."
|
||||
@@ -73,4 +74,4 @@ jobs:
|
||||
run: |
|
||||
docker-compose -f "docker-compose.dev.yml" down
|
||||
docker stop infisical-api
|
||||
docker remove infisical-api
|
||||
docker remove infisical-api
|
||||
|
@@ -1,7 +1,7 @@
|
||||
ARG POSTHOG_HOST=https://app.posthog.com
|
||||
ARG POSTHOG_API_KEY=posthog-api-key
|
||||
ARG INTERCOM_ID=intercom-id
|
||||
ARG SAML_ORG_SLUG=saml-org-slug-default
|
||||
ARG CAPTCHA_SITE_KEY=captcha-site-key
|
||||
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
@@ -36,8 +36,8 @@ ARG INTERCOM_ID
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
||||
ARG INFISICAL_PLATFORM_VERSION
|
||||
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||
ARG SAML_ORG_SLUG
|
||||
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
|
||||
ARG CAPTCHA_SITE_KEY
|
||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
||||
|
||||
# Build
|
||||
RUN npm run build
|
||||
@@ -113,9 +113,9 @@ ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
||||
ARG INTERCOM_ID=intercom-id
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
||||
ARG SAML_ORG_SLUG
|
||||
ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \
|
||||
BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
|
||||
ARG CAPTCHA_SITE_KEY
|
||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
||||
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||
|
||||
WORKDIR /
|
||||
|
||||
|
@@ -85,13 +85,13 @@ To set up and run Infisical locally, make sure you have Git and Docker installed
|
||||
Linux/macOS:
|
||||
|
||||
```console
|
||||
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.prod.yml up
|
||||
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker compose -f docker-compose.prod.yml up
|
||||
```
|
||||
|
||||
Windows Command Prompt:
|
||||
|
||||
```console
|
||||
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.prod.yml up
|
||||
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker compose -f docker-compose.prod.yml up
|
||||
```
|
||||
|
||||
Create an account at `http://localhost:80`
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { Lock } from "@app/lib/red-lock";
|
||||
|
||||
export const mockKeyStore = (): TKeyStoreFactory => {
|
||||
const store: Record<string, string | number | Buffer> = {};
|
||||
@@ -27,7 +28,10 @@ export const mockKeyStore = (): TKeyStoreFactory => {
|
||||
return 1;
|
||||
},
|
||||
acquireLock: () => {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
return Promise.resolve({
|
||||
release: () => {}
|
||||
}) as Promise<Lock>;
|
||||
},
|
||||
waitTillReady: async () => {}
|
||||
};
|
||||
};
|
||||
|
@@ -35,6 +35,8 @@ const getZodPrimitiveType = (type: string) => {
|
||||
return "z.coerce.number()";
|
||||
case "text":
|
||||
return "z.string()";
|
||||
case "bytea":
|
||||
return "zodBuffer";
|
||||
default:
|
||||
throw new Error(`Invalid type: ${type}`);
|
||||
}
|
||||
@@ -96,10 +98,15 @@ const main = async () => {
|
||||
const columnNames = Object.keys(columns);
|
||||
|
||||
let schema = "";
|
||||
const zodImportSet = new Set<string>();
|
||||
for (let colNum = 0; colNum < columnNames.length; colNum++) {
|
||||
const columnName = columnNames[colNum];
|
||||
const colInfo = columns[columnName];
|
||||
let ztype = getZodPrimitiveType(colInfo.type);
|
||||
if (["zodBuffer"].includes(ztype)) {
|
||||
zodImportSet.add(ztype);
|
||||
}
|
||||
|
||||
// don't put optional on id
|
||||
if (colInfo.defaultValue && columnName !== "id") {
|
||||
const { defaultValue } = colInfo;
|
||||
@@ -121,6 +128,8 @@ const main = async () => {
|
||||
.split("_")
|
||||
.reduce((prev, curr) => prev + `${curr.at(0)?.toUpperCase()}${curr.slice(1).toLowerCase()}`, "");
|
||||
|
||||
const zodImports = Array.from(zodImportSet);
|
||||
|
||||
// the insert and update are changed to zod input type to use default cases
|
||||
writeFileSync(
|
||||
path.join(__dirname, "../src/db/schemas", `${dashcase}.ts`),
|
||||
@@ -131,6 +140,8 @@ const main = async () => {
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
${zodImports.length ? `import { ${zodImports.join(",")} } from \"@app/lib/zod\";` : ""}
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ${pascalCase}Schema = z.object({${schema}});
|
||||
|
21
backend/src/@types/knex.d.ts
vendored
21
backend/src/@types/knex.d.ts
vendored
@@ -98,6 +98,15 @@ import {
|
||||
TIntegrations,
|
||||
TIntegrationsInsert,
|
||||
TIntegrationsUpdate,
|
||||
TKmsKeys,
|
||||
TKmsKeysInsert,
|
||||
TKmsKeysUpdate,
|
||||
TKmsKeyVersions,
|
||||
TKmsKeyVersionsInsert,
|
||||
TKmsKeyVersionsUpdate,
|
||||
TKmsRootConfig,
|
||||
TKmsRootConfigInsert,
|
||||
TKmsRootConfigUpdate,
|
||||
TLdapConfigs,
|
||||
TLdapConfigsInsert,
|
||||
TLdapConfigsUpdate,
|
||||
@@ -176,6 +185,9 @@ import {
|
||||
TSecretImports,
|
||||
TSecretImportsInsert,
|
||||
TSecretImportsUpdate,
|
||||
TSecretReferences,
|
||||
TSecretReferencesInsert,
|
||||
TSecretReferencesUpdate,
|
||||
TSecretRotationOutputs,
|
||||
TSecretRotationOutputsInsert,
|
||||
TSecretRotationOutputsUpdate,
|
||||
@@ -240,7 +252,6 @@ import {
|
||||
TWebhooksInsert,
|
||||
TWebhooksUpdate
|
||||
} from "@app/db/schemas";
|
||||
import { TSecretReferences, TSecretReferencesInsert, TSecretReferencesUpdate } from "@app/db/schemas/secret-references";
|
||||
|
||||
declare module "knex/types/tables" {
|
||||
interface Tables {
|
||||
@@ -514,5 +525,13 @@ declare module "knex/types/tables" {
|
||||
TSecretVersionTagJunctionInsert,
|
||||
TSecretVersionTagJunctionUpdate
|
||||
>;
|
||||
// KMS service
|
||||
[TableName.KmsServerRootConfig]: Knex.CompositeTableType<
|
||||
TKmsRootConfig,
|
||||
TKmsRootConfigInsert,
|
||||
TKmsRootConfigUpdate
|
||||
>;
|
||||
[TableName.KmsKey]: Knex.CompositeTableType<TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate>;
|
||||
[TableName.KmsKeyVersion]: Knex.CompositeTableType<TKmsKeyVersions, TKmsKeyVersionsInsert, TKmsKeyVersionsUpdate>;
|
||||
}
|
||||
}
|
||||
|
56
backend/src/db/migrations/20240603075514_kms.ts
Normal file
56
backend/src/db/migrations/20240603075514_kms.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.KmsServerRootConfig))) {
|
||||
await knex.schema.createTable(TableName.KmsServerRootConfig, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.binary("encryptedRootKey").notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.KmsServerRootConfig);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.KmsKey))) {
|
||||
await knex.schema.createTable(TableName.KmsKey, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.binary("encryptedKey").notNullable();
|
||||
t.string("encryptionAlgorithm").notNullable();
|
||||
t.integer("version").defaultTo(1).notNullable();
|
||||
t.string("description");
|
||||
t.boolean("isDisabled").defaultTo(false);
|
||||
t.boolean("isReserved").defaultTo(true);
|
||||
t.string("projectId");
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.uuid("orgId");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.KmsKey);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.KmsKeyVersion))) {
|
||||
await knex.schema.createTable(TableName.KmsKeyVersion, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.binary("encryptedKey").notNullable();
|
||||
t.integer("version").notNullable();
|
||||
t.uuid("kmsKeyId").notNullable();
|
||||
t.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.KmsKeyVersion);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.KmsServerRootConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.KmsServerRootConfig);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.KmsKeyVersion);
|
||||
await dropOnUpdateTrigger(knex, TableName.KmsKeyVersion);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.KmsKey);
|
||||
await dropOnUpdateTrigger(knex, TableName.KmsKey);
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasConsecutiveFailedPasswordAttempts = await knex.schema.hasColumn(
|
||||
TableName.Users,
|
||||
"consecutiveFailedPasswordAttempts"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Users, (tb) => {
|
||||
if (!hasConsecutiveFailedPasswordAttempts) {
|
||||
tb.integer("consecutiveFailedPasswordAttempts").defaultTo(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasConsecutiveFailedPasswordAttempts = await knex.schema.hasColumn(
|
||||
TableName.Users,
|
||||
"consecutiveFailedPasswordAttempts"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Users, (tb) => {
|
||||
if (hasConsecutiveFailedPasswordAttempts) {
|
||||
tb.dropColumn("consecutiveFailedPasswordAttempts");
|
||||
}
|
||||
});
|
||||
}
|
@@ -30,6 +30,9 @@ export * from "./identity-universal-auths";
|
||||
export * from "./incident-contacts";
|
||||
export * from "./integration-auths";
|
||||
export * from "./integrations";
|
||||
export * from "./kms-key-versions";
|
||||
export * from "./kms-keys";
|
||||
export * from "./kms-root-config";
|
||||
export * from "./ldap-configs";
|
||||
export * from "./ldap-group-maps";
|
||||
export * from "./models";
|
||||
@@ -57,6 +60,7 @@ export * from "./secret-blind-indexes";
|
||||
export * from "./secret-folder-versions";
|
||||
export * from "./secret-folders";
|
||||
export * from "./secret-imports";
|
||||
export * from "./secret-references";
|
||||
export * from "./secret-rotation-outputs";
|
||||
export * from "./secret-rotations";
|
||||
export * from "./secret-scanning-git-risks";
|
||||
|
21
backend/src/db/schemas/kms-key-versions.ts
Normal file
21
backend/src/db/schemas/kms-key-versions.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const KmsKeyVersionsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedKey: zodBuffer,
|
||||
version: z.number(),
|
||||
kmsKeyId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TKmsKeyVersions = z.infer<typeof KmsKeyVersionsSchema>;
|
||||
export type TKmsKeyVersionsInsert = Omit<z.input<typeof KmsKeyVersionsSchema>, TImmutableDBKeys>;
|
||||
export type TKmsKeyVersionsUpdate = Partial<Omit<z.input<typeof KmsKeyVersionsSchema>, TImmutableDBKeys>>;
|
26
backend/src/db/schemas/kms-keys.ts
Normal file
26
backend/src/db/schemas/kms-keys.ts
Normal 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
backend/src/db/schemas/kms-root-config.ts
Normal file
19
backend/src/db/schemas/kms-root-config.ts
Normal 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>>;
|
@@ -81,7 +81,11 @@ export enum TableName {
|
||||
DynamicSecretLease = "dynamic_secret_leases",
|
||||
// junction tables with tags
|
||||
JnSecretTag = "secret_tag_junction",
|
||||
SecretVersionTag = "secret_version_tag_junction"
|
||||
SecretVersionTag = "secret_version_tag_junction",
|
||||
// KMS Service
|
||||
KmsServerRootConfig = "kms_root_config",
|
||||
KmsKey = "kms_keys",
|
||||
KmsKeyVersion = "kms_key_versions"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
|
@@ -25,7 +25,8 @@ export const UsersSchema = z.object({
|
||||
isEmailVerified: z.boolean().default(false).nullable().optional(),
|
||||
consecutiveFailedMfaAttempts: z.number().default(0).nullable().optional(),
|
||||
isLocked: z.boolean().default(false).nullable().optional(),
|
||||
temporaryLockDateEnd: z.date().nullable().optional()
|
||||
temporaryLockDateEnd: z.date().nullable().optional(),
|
||||
consecutiveFailedPasswordAttempts: z.number().default(0).nullable().optional()
|
||||
});
|
||||
|
||||
export type TUsers = z.infer<typeof UsersSchema>;
|
||||
|
@@ -77,7 +77,7 @@ type TLdapConfigServiceFactoryDep = {
|
||||
>;
|
||||
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||
};
|
||||
|
||||
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
|
||||
@@ -510,6 +510,7 @@ export const ldapConfigServiceFactory = ({
|
||||
return newUserAlias;
|
||||
});
|
||||
}
|
||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||
|
||||
const user = await userDAL.transaction(async (tx) => {
|
||||
const newUser = await userDAL.findOne({ id: userAlias.userId }, tx);
|
||||
|
@@ -50,7 +50,7 @@ type TSamlConfigServiceFactoryDep = {
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
@@ -449,6 +449,7 @@ export const samlConfigServiceFactory = ({
|
||||
return newUser;
|
||||
});
|
||||
}
|
||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||
|
||||
const isUserCompleted = Boolean(user.isAccepted);
|
||||
const providerAuthToken = jwt.sign(
|
||||
|
@@ -391,7 +391,7 @@ export const scimServiceFactory = ({
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(org.id);
|
||||
return { user, orgMembership };
|
||||
});
|
||||
|
||||
|
@@ -9,6 +9,15 @@ export enum KeyStorePrefixes {
|
||||
SecretReplication = "secret-replication-import-lock"
|
||||
}
|
||||
|
||||
type TWaitTillReady = {
|
||||
key: string;
|
||||
waitingCb?: () => void;
|
||||
keyCheckCb: (val: string | null) => boolean;
|
||||
waitIteration?: number;
|
||||
delay?: number;
|
||||
jitter?: number;
|
||||
};
|
||||
|
||||
export const keyStoreFactory = (redisUrl: string) => {
|
||||
const redis = new Redis(redisUrl);
|
||||
const redisLock = new Redlock([redis], { retryCount: 2, retryDelay: 200 });
|
||||
@@ -29,6 +38,29 @@ export const keyStoreFactory = (redisUrl: string) => {
|
||||
|
||||
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
|
||||
|
||||
const waitTillReady = async ({
|
||||
key,
|
||||
waitingCb,
|
||||
keyCheckCb,
|
||||
waitIteration = 10,
|
||||
delay = 1000,
|
||||
jitter = 200
|
||||
}: TWaitTillReady) => {
|
||||
let attempts = 0;
|
||||
let isReady = keyCheckCb(await getItem(key));
|
||||
while (!isReady) {
|
||||
if (attempts > waitIteration) return;
|
||||
// eslint-disable-next-line
|
||||
await new Promise((resolve) => {
|
||||
waitingCb?.();
|
||||
setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter)));
|
||||
});
|
||||
attempts += 1;
|
||||
// eslint-disable-next-line
|
||||
isReady = keyCheckCb(await getItem(key, "wait_till_ready"));
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
setItem,
|
||||
getItem,
|
||||
@@ -37,6 +69,7 @@ export const keyStoreFactory = (redisUrl: string) => {
|
||||
incrementBy,
|
||||
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
|
||||
return redisLock.acquire(resources, duration, settings);
|
||||
}
|
||||
},
|
||||
waitTillReady
|
||||
};
|
||||
};
|
||||
|
@@ -386,6 +386,8 @@ export const SECRET_IMPORTS = {
|
||||
environment: "The slug of the environment to import into.",
|
||||
path: "The path to import into.",
|
||||
workspaceId: "The ID of the project you are working in.",
|
||||
isReplication:
|
||||
"When true, secrets from the source will be automatically sent to the destination. If approval policies exist at the destination, the secrets will be sent as approval requests instead of being applied immediately.",
|
||||
import: {
|
||||
environment: "The slug of the environment to import from.",
|
||||
path: "The path to import from."
|
||||
@@ -674,7 +676,10 @@ export const INTEGRATION = {
|
||||
secretGCPLabel: "The label for GCP secrets.",
|
||||
secretAWSTag: "The tags for AWS secrets.",
|
||||
kmsKeyId: "The ID of the encryption key from AWS KMS.",
|
||||
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store."
|
||||
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
|
||||
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: {
|
||||
|
@@ -75,6 +75,7 @@ const envSchema = z
|
||||
.optional()
|
||||
.default(process.env.URL_GITLAB_LOGIN ?? GITLAB_URL)
|
||||
), // fallback since URL_GITLAB_LOGIN has been renamed
|
||||
DEFAULT_SAML_ORG_SLUG: zpStr(z.string().optional()).default(process.env.NEXT_PUBLIC_SAML_ORG_SLUG),
|
||||
// integration client secrets
|
||||
// heroku
|
||||
CLIENT_ID_HEROKU: zpStr(z.string().optional()),
|
||||
@@ -119,7 +120,8 @@ const envSchema = z
|
||||
.transform((val) => val === "true")
|
||||
.optional(),
|
||||
INFISICAL_CLOUD: zodStrBool.default("false"),
|
||||
MAINTENANCE_MODE: zodStrBool.default("false")
|
||||
MAINTENANCE_MODE: zodStrBool.default("false"),
|
||||
CAPTCHA_SECRET: zpStr(z.string().optional())
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
@@ -131,7 +133,8 @@ const envSchema = z
|
||||
isSecretScanningConfigured:
|
||||
Boolean(data.SECRET_SCANNING_GIT_APP_ID) &&
|
||||
Boolean(data.SECRET_SCANNING_PRIVATE_KEY) &&
|
||||
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET)
|
||||
Boolean(data.SECRET_SCANNING_WEBHOOK_SECRET),
|
||||
samlDefaultOrgSlug: data.DEFAULT_SAML_ORG_SLUG
|
||||
}));
|
||||
|
||||
let envCfg: Readonly<z.infer<typeof envSchema>>;
|
||||
|
49
backend/src/lib/crypto/cipher/cipher.ts
Normal file
49
backend/src/lib/crypto/cipher/cipher.ts
Normal 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
backend/src/lib/crypto/cipher/index.ts
Normal file
2
backend/src/lib/crypto/cipher/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { symmetricCipherService } from "./cipher";
|
||||
export { SymmetricEncryption } from "./types";
|
9
backend/src/lib/crypto/cipher/types.ts
Normal file
9
backend/src/lib/crypto/cipher/types.ts
Normal 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;
|
||||
};
|
@@ -11,6 +11,8 @@ import { getConfig } from "../config/env";
|
||||
export const decodeBase64 = (s: string) => naclUtils.decodeBase64(s);
|
||||
export const encodeBase64 = (u: Uint8Array) => naclUtils.encodeBase64(u);
|
||||
|
||||
export const randomSecureBytes = (length = 32) => crypto.randomBytes(length);
|
||||
|
||||
export type TDecryptSymmetricInput = {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
|
@@ -9,7 +9,8 @@ export {
|
||||
encryptAsymmetric,
|
||||
encryptSymmetric,
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
generateAsymmetricKeyPair
|
||||
generateAsymmetricKeyPair,
|
||||
randomSecureBytes
|
||||
} from "./encryption";
|
||||
export {
|
||||
decryptIntegrationAuths,
|
||||
|
@@ -7,3 +7,7 @@ export const zpStr = <T extends ZodTypeAny>(schema: T, opt: { stripNull: boolean
|
||||
if (typeof val !== "string") return val;
|
||||
return val.trim() || undefined;
|
||||
}, schema);
|
||||
|
||||
export const zodBuffer = z.custom<Buffer>((data) => Buffer.isBuffer(data) || data instanceof Uint8Array, {
|
||||
message: "Expected binary data (Buffer Or Uint8Array)"
|
||||
});
|
||||
|
@@ -97,6 +97,9 @@ import { integrationDALFactory } from "@app/services/integration/integration-dal
|
||||
import { integrationServiceFactory } from "@app/services/integration/integration-service";
|
||||
import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal";
|
||||
import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||
import { kmsDALFactory } from "@app/services/kms/kms-dal";
|
||||
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
||||
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||
@@ -261,6 +264,9 @@ export const registerRoutes = async (
|
||||
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
||||
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
||||
|
||||
const kmsDAL = kmsDALFactory(db);
|
||||
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
orgRoleDAL,
|
||||
@@ -269,6 +275,12 @@ export const registerRoutes = async (
|
||||
projectDAL
|
||||
});
|
||||
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
|
||||
const kmsService = kmsServiceFactory({
|
||||
kmsRootConfigDAL,
|
||||
keyStore,
|
||||
kmsDAL
|
||||
});
|
||||
|
||||
const trustedIpService = trustedIpServiceFactory({
|
||||
licenseService,
|
||||
projectDAL,
|
||||
@@ -823,6 +835,7 @@ export const registerRoutes = async (
|
||||
|
||||
await telemetryQueue.startTelemetryCheck();
|
||||
await dailyResourceCleanUp.startCleanUp();
|
||||
await kmsService.startService();
|
||||
|
||||
// inject all services
|
||||
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||
@@ -906,7 +919,8 @@ export const registerRoutes = async (
|
||||
emailConfigured: z.boolean().optional(),
|
||||
inviteOnlySignup: z.boolean().optional(),
|
||||
redisConfigured: z.boolean().optional(),
|
||||
secretScanningConfigured: z.boolean().optional()
|
||||
secretScanningConfigured: z.boolean().optional(),
|
||||
samlDefaultOrgSlug: z.string().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -919,7 +933,8 @@ export const registerRoutes = async (
|
||||
emailConfigured: cfg.isSmtpConfigured,
|
||||
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
|
||||
redisConfigured: cfg.isRedisConfigured,
|
||||
secretScanningConfigured: cfg.isSecretScanningConfigured
|
||||
secretScanningConfigured: cfg.isSecretScanningConfigured,
|
||||
samlDefaultOrgSlug: cfg.samlDefaultOrgSlug
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@@ -8,7 +8,7 @@ import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { IntegrationMappingBehavior } from "@app/services/integration-auth/integration-list";
|
||||
import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
|
||||
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -46,36 +46,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
path: z.string().trim().optional().describe(INTEGRATION.CREATE.path),
|
||||
region: z.string().trim().optional().describe(INTEGRATION.CREATE.region),
|
||||
scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope),
|
||||
metadata: z
|
||||
.object({
|
||||
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
|
||||
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
|
||||
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
|
||||
mappingBehavior: z
|
||||
.nativeEnum(IntegrationMappingBehavior)
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.mappingBehavior),
|
||||
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
|
||||
secretGCPLabel: z
|
||||
.object({
|
||||
labelName: z.string(),
|
||||
labelValue: z.string()
|
||||
})
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
|
||||
secretAWSTag: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
||||
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
|
||||
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete)
|
||||
})
|
||||
.default({})
|
||||
metadata: IntegrationMetadataSchema.default({})
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -161,33 +132,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment),
|
||||
owner: z.string().trim().describe(INTEGRATION.UPDATE.owner),
|
||||
environment: z.string().trim().describe(INTEGRATION.UPDATE.environment),
|
||||
metadata: z
|
||||
.object({
|
||||
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
|
||||
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
|
||||
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
|
||||
mappingBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.mappingBehavior),
|
||||
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
|
||||
secretGCPLabel: z
|
||||
.object({
|
||||
labelName: z.string(),
|
||||
labelValue: z.string()
|
||||
})
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
|
||||
secretAWSTag: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
|
||||
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
|
||||
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete)
|
||||
})
|
||||
.optional()
|
||||
metadata: IntegrationMetadataSchema.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -30,7 +30,7 @@ export const registerSecretImportRouter = async (server: FastifyZodProvider) =>
|
||||
environment: z.string().trim().describe(SECRET_IMPORTS.CREATE.import.environment),
|
||||
path: z.string().trim().transform(removeTrailingSlash).describe(SECRET_IMPORTS.CREATE.import.path)
|
||||
}),
|
||||
isReplication: z.boolean().default(false)
|
||||
isReplication: z.boolean().default(false).describe(SECRET_IMPORTS.CREATE.isReplication)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -80,7 +80,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
body: z.object({
|
||||
email: z.string().trim(),
|
||||
providerAuthToken: z.string().trim().optional(),
|
||||
clientProof: z.string().trim()
|
||||
clientProof: z.string().trim(),
|
||||
captchaToken: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.discriminatedUnion("mfaEnabled", [
|
||||
@@ -106,6 +107,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const data = await server.services.login.loginExchangeClientProof({
|
||||
captchaToken: req.body.captchaToken,
|
||||
email: req.body.email,
|
||||
ip: req.realIp,
|
||||
userAgent,
|
||||
|
@@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
|
||||
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
@@ -176,12 +177,16 @@ export const authLoginServiceFactory = ({
|
||||
clientProof,
|
||||
ip,
|
||||
userAgent,
|
||||
providerAuthToken
|
||||
providerAuthToken,
|
||||
captchaToken
|
||||
}: TLoginClientProofDTO) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const userEnc = await userDAL.findUserEncKeyByUsername({
|
||||
username: email
|
||||
});
|
||||
if (!userEnc) throw new Error("Failed to find user");
|
||||
const user = await userDAL.findById(userEnc.userId);
|
||||
const cfg = getConfig();
|
||||
|
||||
let authMethod = AuthMethod.EMAIL;
|
||||
@@ -196,6 +201,31 @@ export const authLoginServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
user.consecutiveFailedPasswordAttempts &&
|
||||
user.consecutiveFailedPasswordAttempts >= 10 &&
|
||||
Boolean(appCfg.CAPTCHA_SECRET)
|
||||
) {
|
||||
if (!captchaToken) {
|
||||
throw new BadRequestError({
|
||||
name: "Captcha Required",
|
||||
message: "Accomplish the required captcha by logging in via Web"
|
||||
});
|
||||
}
|
||||
|
||||
// validate captcha token
|
||||
const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
|
||||
response: captchaToken,
|
||||
secret: appCfg.CAPTCHA_SECRET
|
||||
});
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new BadRequestError({
|
||||
name: "Invalid Captcha"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
|
||||
const isValidClientProof = await srpCheckClientProof(
|
||||
userEnc.salt,
|
||||
@@ -204,15 +234,31 @@ export const authLoginServiceFactory = ({
|
||||
userEnc.clientPublicKey,
|
||||
clientProof
|
||||
);
|
||||
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
|
||||
|
||||
if (!isValidClientProof) {
|
||||
await userDAL.update(
|
||||
{ id: userEnc.userId },
|
||||
{
|
||||
$incr: {
|
||||
consecutiveFailedPasswordAttempts: 1
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
throw new Error("Failed to authenticate. Try again?");
|
||||
}
|
||||
|
||||
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
|
||||
serverPrivateKey: null,
|
||||
clientPublicKey: null
|
||||
});
|
||||
|
||||
await userDAL.updateById(userEnc.userId, {
|
||||
consecutiveFailedPasswordAttempts: 0
|
||||
});
|
||||
|
||||
// send multi factor auth token if they it enabled
|
||||
if (userEnc.isMfaEnabled && userEnc.email) {
|
||||
const user = await userDAL.findById(userEnc.userId);
|
||||
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||
|
||||
const mfaToken = jwt.sign(
|
||||
|
@@ -12,6 +12,7 @@ export type TLoginClientProofDTO = {
|
||||
providerAuthToken?: string;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
captchaToken?: string;
|
||||
};
|
||||
|
||||
export type TVerifyMfaTokenDTO = {
|
||||
|
@@ -31,6 +31,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
|
||||
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||
import {
|
||||
IntegrationInitialSyncBehavior,
|
||||
IntegrationMappingBehavior,
|
||||
@@ -1363,38 +1364,41 @@ const syncSecretsGitHub = async ({
|
||||
}
|
||||
}
|
||||
|
||||
for await (const encryptedSecret of encryptedSecrets) {
|
||||
if (
|
||||
!(encryptedSecret.name in secrets) &&
|
||||
!(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) &&
|
||||
!(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix))
|
||||
) {
|
||||
switch (integration.scope) {
|
||||
case GithubScope.Org: {
|
||||
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
|
||||
org: integration.owner as string,
|
||||
secret_name: encryptedSecret.name
|
||||
});
|
||||
break;
|
||||
}
|
||||
case GithubScope.Env: {
|
||||
await octokit.request(
|
||||
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
|
||||
{
|
||||
repository_id: Number(integration.appId),
|
||||
environment_name: integration.targetEnvironmentId as string,
|
||||
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||
if (metadata.shouldEnableDelete) {
|
||||
for await (const encryptedSecret of encryptedSecrets) {
|
||||
if (
|
||||
!(encryptedSecret.name in secrets) &&
|
||||
!(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) &&
|
||||
!(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix))
|
||||
) {
|
||||
switch (integration.scope) {
|
||||
case GithubScope.Org: {
|
||||
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
|
||||
org: integration.owner as string,
|
||||
secret_name: encryptedSecret.name
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
|
||||
owner: integration.owner as string,
|
||||
repo: integration.app as string,
|
||||
secret_name: encryptedSecret.name
|
||||
});
|
||||
break;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case GithubScope.Env: {
|
||||
await octokit.request(
|
||||
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
|
||||
{
|
||||
repository_id: Number(integration.appId),
|
||||
environment_name: integration.targetEnvironmentId as string,
|
||||
secret_name: encryptedSecret.name
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
|
||||
owner: integration.owner as string,
|
||||
repo: integration.app as string,
|
||||
secret_name: encryptedSecret.name
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1917,13 +1921,13 @@ const syncSecretsGitLab = async ({
|
||||
return allEnvVariables;
|
||||
};
|
||||
|
||||
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||
const allEnvVariables = await getAllEnvVariables(integration?.appId as string, accessToken);
|
||||
const getSecretsRes: GitLabSecret[] = allEnvVariables
|
||||
.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
|
||||
.filter((gitLabSecret) => {
|
||||
let isValid = true;
|
||||
|
||||
const metadata = z.record(z.any()).parse(integration.metadata);
|
||||
if (metadata.secretPrefix && !gitLabSecret.key.startsWith(metadata.secretPrefix)) {
|
||||
isValid = false;
|
||||
}
|
||||
@@ -1943,8 +1947,8 @@ const syncSecretsGitLab = async ({
|
||||
{
|
||||
key,
|
||||
value: secrets[key].value,
|
||||
protected: false,
|
||||
masked: false,
|
||||
protected: Boolean(metadata.shouldProtectSecrets),
|
||||
masked: Boolean(metadata.shouldMaskSecrets),
|
||||
raw: false,
|
||||
environment_scope: integration.targetEnvironment
|
||||
},
|
||||
@@ -1961,7 +1965,9 @@ const syncSecretsGitLab = async ({
|
||||
`${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
|
||||
{
|
||||
...existingSecret,
|
||||
value: secrets[existingSecret.key].value
|
||||
value: secrets[existingSecret.key].value,
|
||||
protected: Boolean(metadata.shouldProtectSecrets),
|
||||
masked: Boolean(metadata.shouldMaskSecrets)
|
||||
},
|
||||
{
|
||||
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"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
37
backend/src/services/integration/integration-schema.ts
Normal file
37
backend/src/services/integration/integration-schema.ts
Normal file
@@ -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)
|
||||
});
|
@@ -29,6 +29,9 @@ export type TCreateIntegrationDTO = {
|
||||
}[];
|
||||
kmsKeyId?: string;
|
||||
shouldDisableDelete?: boolean;
|
||||
shouldMaskSecrets?: boolean;
|
||||
shouldProtectSecrets?: boolean;
|
||||
shouldEnableDelete?: boolean;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@@ -54,6 +57,7 @@ export type TUpdateIntegrationDTO = {
|
||||
}[];
|
||||
kmsKeyId?: string;
|
||||
shouldDisableDelete?: boolean;
|
||||
shouldEnableDelete?: boolean;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
|
10
backend/src/services/kms/kms-dal.ts
Normal file
10
backend/src/services/kms/kms-dal.ts
Normal 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;
|
||||
};
|
10
backend/src/services/kms/kms-root-config-dal.ts
Normal file
10
backend/src/services/kms/kms-root-config-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TKmsRootConfigDALFactory = ReturnType<typeof kmsRootConfigDALFactory>;
|
||||
|
||||
export const kmsRootConfigDALFactory = (db: TDbClient) => {
|
||||
const kmsOrm = ormify(db, TableName.KmsServerRootConfig);
|
||||
return kmsOrm;
|
||||
};
|
126
backend/src/services/kms/kms-service.ts
Normal file
126
backend/src/services/kms/kms-service.ts
Normal 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
backend/src/services/kms/kms-types.ts
Normal file
15
backend/src/services/kms/kms-types.ts
Normal 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;
|
||||
};
|
@@ -315,6 +315,7 @@ export const orgServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
await licenseService.updateSubscriptionOrgMemberCount(org.id);
|
||||
await orgBotDAL.create(
|
||||
{
|
||||
name: org.name,
|
||||
|
@@ -395,7 +395,8 @@ export const decryptSecretRaw = (
|
||||
type: secret.type,
|
||||
_id: secret.id,
|
||||
id: secret.id,
|
||||
user: secret.userId
|
||||
user: secret.userId,
|
||||
skipMultilineEncoding: secret.skipMultilineEncoding
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -1,4 +1,6 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||
@@ -567,11 +569,14 @@ export const secretQueueFactory = ({
|
||||
isSynced: true
|
||||
});
|
||||
} 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, {
|
||||
lastSyncJobId: job.id,
|
||||
lastUsed: new Date(),
|
||||
syncMessage: (err as Error)?.message,
|
||||
syncMessage: message,
|
||||
isSynced: false
|
||||
});
|
||||
}
|
||||
|
@@ -952,15 +952,49 @@ export const secretServiceFactory = ({
|
||||
});
|
||||
|
||||
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
|
||||
const decryptedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({
|
||||
...el,
|
||||
secrets: importedSecrets.map((sec) =>
|
||||
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
|
||||
const decryptedImportSecrets = importedSecrets.map((sec) =>
|
||||
decryptSecretRaw(
|
||||
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
|
||||
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) {
|
||||
const expandSecrets = interpolateSecrets({
|
||||
@@ -1011,12 +1045,12 @@ export const secretServiceFactory = ({
|
||||
await batchSecretsExpand(decryptedSecrets);
|
||||
|
||||
// expand imports by batch
|
||||
await Promise.all(decryptedImports.map((decryptedImport) => batchSecretsExpand(decryptedImport.secrets)));
|
||||
await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets)));
|
||||
}
|
||||
|
||||
return {
|
||||
secrets: decryptedSecrets,
|
||||
imports: decryptedImports
|
||||
imports: processedImports
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -496,7 +496,6 @@ To enable auto redeployment you simply have to add the following annotation to t
|
||||
```yaml
|
||||
secrets.infisical.com/auto-reload: "true"
|
||||
```
|
||||
|
||||
<Accordion title="Deployment example with auto redeploy enabled">
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
@@ -527,7 +526,11 @@ spec:
|
||||
- containerPort: 80
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Info>
|
||||
#### How it works
|
||||
When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update.
|
||||
Then, for each deployment that has this annotation present, a rolling update will be triggered.
|
||||
</Info>
|
||||
## Global configuration
|
||||
|
||||
To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap.
|
||||
|
@@ -384,6 +384,7 @@
|
||||
"pages": [
|
||||
"sdks/languages/node",
|
||||
"sdks/languages/python",
|
||||
"sdks/languages/go",
|
||||
"sdks/languages/java",
|
||||
"sdks/languages/csharp"
|
||||
]
|
||||
|
438
docs/sdks/languages/go.mdx
Normal file
438
docs/sdks/languages/go.mdx
Normal 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>
|
@@ -318,6 +318,11 @@ SMTP_FROM_NAME=Infisical
|
||||
By default, users can only login via email/password based login method.
|
||||
To login into Infisical with OAuth providers such as Google, configure the associated variables.
|
||||
|
||||
<ParamField query="DEFAULT_SAML_ORG_SLUG" type="string">
|
||||
|
||||
When set, all visits to the Infisical login page will automatically redirect users of your Infisical instance to the SAML identity provider associated with the specified organization slug.
|
||||
</ParamField>
|
||||
|
||||
<Accordion title="Google">
|
||||
Follow detailed guide to configure [Google SSO](/documentation/platform/sso/google)
|
||||
|
||||
@@ -369,11 +374,6 @@ To login into Infisical with OAuth providers such as Google, configure the assoc
|
||||
information.
|
||||
</Accordion>
|
||||
|
||||
<ParamField query="NEXT_PUBLIC_SAML_ORG_SLUG" type="string">
|
||||
Configure SAML organization slug to automatically redirect all users of your
|
||||
Infisical instance to the identity provider.
|
||||
</ParamField>
|
||||
|
||||
## Native secret integrations
|
||||
|
||||
To help you sync secrets from Infisical to services such as Github and Gitlab, Infisical provides native integrations out of the box.
|
||||
|
@@ -2,6 +2,7 @@ ARG POSTHOG_HOST=https://app.posthog.com
|
||||
ARG POSTHOG_API_KEY=posthog-api-key
|
||||
ARG INTERCOM_ID=intercom-id
|
||||
ARG NEXT_INFISICAL_PLATFORM_VERSION=next-infisical-platform-version
|
||||
ARG CAPTCHA_SITE_KEY=captcha-site-key
|
||||
|
||||
FROM node:16-alpine AS deps
|
||||
# Install dependencies only when needed. Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
@@ -31,6 +32,8 @@ ARG POSTHOG_API_KEY
|
||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||
ARG INTERCOM_ID
|
||||
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
||||
ARG CAPTCHA_SITE_KEY
|
||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY
|
||||
|
||||
# Build
|
||||
RUN npm run build
|
||||
@@ -57,7 +60,9 @@ ENV NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG \
|
||||
BAKED_NEXT_PUBLIC_SAML_ORG_SLUG=$SAML_ORG_SLUG
|
||||
ARG NEXT_INFISICAL_PLATFORM_VERSION
|
||||
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION=$NEXT_INFISICAL_PLATFORM_VERSION
|
||||
|
||||
ARG CAPTCHA_SITE_KEY
|
||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
||||
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||
COPY --chown=nextjs:nodejs --chmod=555 scripts ./scripts
|
||||
COPY --from=builder /app/public ./public
|
||||
RUN chown nextjs:nodejs ./public/data
|
||||
|
@@ -1,13 +1,12 @@
|
||||
|
||||
const path = require("path");
|
||||
|
||||
const ContentSecurityPolicy = `
|
||||
default-src 'self';
|
||||
script-src 'self' https://app.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com 'unsafe-inline' 'unsafe-eval';
|
||||
style-src 'self' https://rsms.me 'unsafe-inline';
|
||||
script-src 'self' https://app.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
|
||||
style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
|
||||
child-src https://api.stripe.com;
|
||||
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/;
|
||||
connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://127.0.0.1:*;
|
||||
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/ https://hcaptcha.com https://*.hcaptcha.com;
|
||||
connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://127.0.0.1:* https://hcaptcha.com https://*.hcaptcha.com;
|
||||
img-src 'self' https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:;
|
||||
media-src https://js.intercomcdn.com;
|
||||
font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com;
|
||||
|
20
frontend/package-lock.json
generated
20
frontend/package-lock.json
generated
@@ -4,7 +4,6 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@casl/react": "^3.1.0",
|
||||
@@ -19,6 +18,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@hcaptcha/react-hcaptcha": "^1.10.1",
|
||||
"@headlessui/react": "^1.7.7",
|
||||
"@hookform/resolvers": "^2.9.10",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
@@ -3200,6 +3200,24 @@
|
||||
"react": ">=16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@hcaptcha/loader": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@hcaptcha/loader/-/loader-1.2.4.tgz",
|
||||
"integrity": "sha512-3MNrIy/nWBfyVVvMPBKdKrX7BeadgiimW0AL/a/8TohNtJqxoySKgTJEXOQvYwlHemQpUzFrIsK74ody7JiMYw=="
|
||||
},
|
||||
"node_modules/@hcaptcha/react-hcaptcha": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.10.1.tgz",
|
||||
"integrity": "sha512-P0en4gEZAecah7Pt3WIaJO2gFlaLZKkI0+Tfdg8fNqsDxqT9VytZWSkH4WAkiPRULK1QcGgUZK+J56MXYmPifw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.9",
|
||||
"@hcaptcha/loader": "^1.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3.0",
|
||||
"react-dom": ">= 16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "1.7.18",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz",
|
||||
|
@@ -26,6 +26,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@hcaptcha/react-hcaptcha": "^1.10.1",
|
||||
"@headlessui/react": "^1.7.7",
|
||||
"@hookform/resolvers": "^2.9.10",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
|
@@ -4,7 +4,7 @@ scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY
|
||||
|
||||
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID"
|
||||
|
||||
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG"
|
||||
scripts/replace-standalone-build-variable.sh "$BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY" "$NEXT_PUBLIC_CAPTCHA_SITE_KEY"
|
||||
|
||||
if [ "$TELEMETRY_ENABLED" != "false" ]; then
|
||||
echo "Telemetry is enabled"
|
||||
|
@@ -6,6 +6,8 @@ scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTER
|
||||
|
||||
scripts/replace-variable.sh "$BAKED_NEXT_SAML_ORG_SLUG" "$NEXT_PUBLIC_SAML_ORG_SLUG"
|
||||
|
||||
scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY" "$NEXT_PUBLIC_CAPTCHA_SITE_KEY"
|
||||
|
||||
if [ "$TELEMETRY_ENABLED" != "false" ]; then
|
||||
echo "Telemetry is enabled"
|
||||
scripts/set-telemetry.sh true
|
||||
|
@@ -30,11 +30,13 @@ export interface IsCliLoginSuccessful {
|
||||
const attemptLogin = async ({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken
|
||||
providerAuthToken,
|
||||
captchaToken
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
providerAuthToken?: string;
|
||||
captchaToken?: string;
|
||||
}): Promise<IsCliLoginSuccessful> => {
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -70,7 +72,8 @@ const attemptLogin = async ({
|
||||
} = await login2({
|
||||
email,
|
||||
clientProof,
|
||||
providerAuthToken
|
||||
providerAuthToken,
|
||||
captchaToken
|
||||
});
|
||||
if (mfaEnabled) {
|
||||
// case: MFA is enabled
|
||||
|
@@ -22,11 +22,13 @@ interface IsLoginSuccessful {
|
||||
const attemptLogin = async ({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken
|
||||
providerAuthToken,
|
||||
captchaToken
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
providerAuthToken?: string;
|
||||
captchaToken?: string;
|
||||
}): Promise<IsLoginSuccessful> => {
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
// eslint-disable-next-line new-cap
|
||||
@@ -58,6 +60,7 @@ const attemptLogin = async ({
|
||||
iv,
|
||||
tag
|
||||
} = await login2({
|
||||
captchaToken,
|
||||
email,
|
||||
clientProof,
|
||||
providerAuthToken
|
||||
|
@@ -2,5 +2,6 @@ const ENV = process.env.NEXT_PUBLIC_ENV! || "development"; // investigate
|
||||
const POSTHOG_API_KEY = process.env.NEXT_PUBLIC_POSTHOG_API_KEY!;
|
||||
const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
|
||||
const INTERCOMid = process.env.NEXT_PUBLIC_INTERCOMid!;
|
||||
const CAPTCHA_SITE_KEY = process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY!;
|
||||
|
||||
export { ENV, INTERCOMid, POSTHOG_API_KEY, POSTHOG_HOST };
|
||||
export { CAPTCHA_SITE_KEY, ENV, INTERCOMid, POSTHOG_API_KEY, POSTHOG_HOST };
|
||||
|
@@ -62,6 +62,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
<SelectPrimitive.Content
|
||||
className={twMerge(
|
||||
"relative top-1 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md",
|
||||
position === "popper" && "max-h-72",
|
||||
dropdownContainerClassName
|
||||
)}
|
||||
position={position}
|
||||
@@ -113,7 +114,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
||||
outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
|
||||
isSelected && "bg-primary",
|
||||
isDisabled &&
|
||||
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
|
||||
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
|
||||
className
|
||||
)}
|
||||
ref={forwardedRef}
|
||||
|
5
frontend/src/helpers/string.ts
Normal file
5
frontend/src/helpers/string.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const removeTrailingSlash = (str: string) => {
|
||||
if (str === "/") return str;
|
||||
|
||||
return str.endsWith("/") ? str.slice(0, -1) : str;
|
||||
};
|
@@ -30,6 +30,7 @@ export type Login1DTO = {
|
||||
};
|
||||
|
||||
export type Login2DTO = {
|
||||
captchaToken?: string;
|
||||
email: string;
|
||||
clientProof: string;
|
||||
providerAuthToken?: string;
|
||||
|
@@ -73,6 +73,9 @@ export const useCreateIntegration = () => {
|
||||
}[];
|
||||
kmsKeyId?: string;
|
||||
shouldDisableDelete?: boolean;
|
||||
shouldMaskSecrets?: boolean;
|
||||
shouldProtectSecrets?: boolean;
|
||||
shouldEnableDelete?: boolean;
|
||||
};
|
||||
}) => {
|
||||
const {
|
||||
|
@@ -4,4 +4,5 @@ export type ServerStatus = {
|
||||
emailConfigured: boolean;
|
||||
secretScanningConfigured: boolean;
|
||||
redisConfigured: boolean;
|
||||
samlDefaultOrgSlug: boolean
|
||||
};
|
||||
|
@@ -7,7 +7,15 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
||||
import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch
|
||||
} from "../../../components/v2";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
@@ -34,6 +42,7 @@ export default function CloudflarePagesIntegrationPage() {
|
||||
const [targetApp, setTargetApp] = useState("");
|
||||
const [targetAppId, setTargetAppId] = useState("");
|
||||
const [targetEnvironment, setTargetEnvironment] = useState("");
|
||||
const [shouldAutoRedeploy, setShouldAutoRedeploy] = useState(false);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -69,7 +78,10 @@ export default function CloudflarePagesIntegrationPage() {
|
||||
appId: targetAppId,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
targetEnvironment,
|
||||
secretPath
|
||||
secretPath,
|
||||
metadata: {
|
||||
shouldAutoRedeploy
|
||||
}
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -169,6 +181,15 @@ export default function CloudflarePagesIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<div className="mb-[2.36rem] ml-1 px-6">
|
||||
<Switch
|
||||
id="redeploy-cloudflare-pages"
|
||||
onCheckedChange={(isChecked: boolean) => setShouldAutoRedeploy(isChecked)}
|
||||
isChecked={shouldAutoRedeploy}
|
||||
>
|
||||
Auto-redeploy service upon secret change
|
||||
</Switch>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
|
@@ -33,6 +33,7 @@ import {
|
||||
Input,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
@@ -59,7 +60,7 @@ const schema = yup.object({
|
||||
selectedSourceEnvironment: yup.string().trim().required("Project Environment is required"),
|
||||
secretPath: yup.string().trim().required("Secrets Path is required"),
|
||||
secretSuffix: yup.string().trim().optional(),
|
||||
|
||||
shouldEnableDelete: yup.boolean().optional(),
|
||||
scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(),
|
||||
|
||||
repoIds: yup.mixed().when("scope", {
|
||||
@@ -98,7 +99,6 @@ type FormData = yup.InferType<typeof schema>;
|
||||
export default function GitHubCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
const { mutateAsync } = useCreateIntegration();
|
||||
|
||||
|
||||
const integrationAuthId =
|
||||
(queryString.parse(router.asPath.split("?")[1]).integrationAuthId as string) ?? "";
|
||||
@@ -120,7 +120,8 @@ export default function GitHubCreateIntegrationPage() {
|
||||
defaultValues: {
|
||||
secretPath: "/",
|
||||
scope: "github-repo",
|
||||
repoIds: []
|
||||
repoIds: [],
|
||||
shouldEnableDelete: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -177,7 +178,8 @@ export default function GitHubCreateIntegrationPage() {
|
||||
app: targetApp.name, // repo name
|
||||
owner: targetApp.owner, // repo owner
|
||||
metadata: {
|
||||
secretSuffix: data.secretSuffix
|
||||
secretSuffix: data.secretSuffix,
|
||||
shouldEnableDelete: data.shouldEnableDelete
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -194,7 +196,8 @@ export default function GitHubCreateIntegrationPage() {
|
||||
scope: data.scope,
|
||||
owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name,
|
||||
metadata: {
|
||||
secretSuffix: data.secretSuffix
|
||||
secretSuffix: data.secretSuffix,
|
||||
shouldEnableDelete: data.shouldEnableDelete
|
||||
}
|
||||
});
|
||||
break;
|
||||
@@ -211,7 +214,8 @@ export default function GitHubCreateIntegrationPage() {
|
||||
owner: repoOwner,
|
||||
targetEnvironmentId: data.envId,
|
||||
metadata: {
|
||||
secretSuffix: data.secretSuffix
|
||||
secretSuffix: data.secretSuffix,
|
||||
shouldEnableDelete: data.shouldEnableDelete
|
||||
}
|
||||
});
|
||||
break;
|
||||
@@ -546,6 +550,21 @@ export default function GitHubCreateIntegrationPage() {
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<div className="ml-1 mb-5">
|
||||
<Controller
|
||||
control={control}
|
||||
name="shouldEnableDelete"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch
|
||||
id="delete-github-option"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
>
|
||||
Delete secrets in Github that are not in Infisical
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretSuffix"
|
||||
|
@@ -25,6 +25,7 @@ import {
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
@@ -58,7 +59,9 @@ const schema = yup.object({
|
||||
targetAppId: yup.string().required("GitLab project is required"),
|
||||
targetEnvironment: yup.string(),
|
||||
secretPrefix: yup.string(),
|
||||
secretSuffix: yup.string()
|
||||
secretSuffix: yup.string(),
|
||||
shouldMaskSecrets: yup.boolean(),
|
||||
shouldProtectSecrets: yup.boolean()
|
||||
});
|
||||
|
||||
type FormData = yup.InferType<typeof schema>;
|
||||
@@ -138,7 +141,9 @@ export default function GitLabCreateIntegrationPage() {
|
||||
targetAppId,
|
||||
targetEnvironment,
|
||||
secretPrefix,
|
||||
secretSuffix
|
||||
secretSuffix,
|
||||
shouldMaskSecrets,
|
||||
shouldProtectSecrets
|
||||
}: FormData) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@@ -156,7 +161,9 @@ export default function GitLabCreateIntegrationPage() {
|
||||
secretPath,
|
||||
metadata: {
|
||||
secretPrefix,
|
||||
secretSuffix
|
||||
secretSuffix,
|
||||
shouldMaskSecrets,
|
||||
shouldProtectSecrets
|
||||
}
|
||||
});
|
||||
|
||||
@@ -390,6 +397,36 @@ export default function GitLabCreateIntegrationPage() {
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="pb-[14.25rem]"
|
||||
>
|
||||
<div className="ml-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="shouldMaskSecrets"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch
|
||||
id="should-mask-secrets"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
>
|
||||
<div className="max-w-md">Mark Infisical secrets in Gitlab as 'Masked' secrets</div>
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-1 mt-4 mb-5">
|
||||
<Controller
|
||||
control={control}
|
||||
name="shouldProtectSecrets"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch
|
||||
id="should-protect-secrets"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
>
|
||||
Mark Infisical secrets in Gitlab as 'Protected' secrets
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPrefix"
|
||||
|
@@ -1,17 +1,20 @@
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { FormEvent, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
|
||||
import Error from "@app/components/basic/Error";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
|
||||
import { Button, Input } from "@app/components/v2";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { useFetchServerStatus } from "@app/hooks/api";
|
||||
|
||||
import { navigateUserToSelectOrg } from "../../Login.utils";
|
||||
|
||||
@@ -31,21 +34,18 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
const { config } = useServerConfig();
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
const [captchaToken, setCaptchaToken] = useState("");
|
||||
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
|
||||
const captchaRef = useRef<HCaptcha>(null);
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
process.env.NEXT_PUBLIC_SAML_ORG_SLUG &&
|
||||
process.env.NEXT_PUBLIC_SAML_ORG_SLUG !== "saml-org-slug-default"
|
||||
) {
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
window.open(
|
||||
`/api/v1/sso/redirect/saml2/organizations/${process.env.NEXT_PUBLIC_SAML_ORG_SLUG}${
|
||||
callbackPort ? `?callback_port=${callbackPort}` : ""
|
||||
}`
|
||||
);
|
||||
window.close();
|
||||
}
|
||||
}, []);
|
||||
if (serverDetails?.samlDefaultOrgSlug){
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${serverDetails?.samlDefaultOrgSlug}${callbackPort ? `?callback_port=${callbackPort}` : ""}`
|
||||
router.push(redirectUrl);
|
||||
}
|
||||
}, [serverDetails?.samlDefaultOrgSlug]);
|
||||
|
||||
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
@@ -61,7 +61,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
// attemptCliLogin
|
||||
const isCliLoginSuccessful = await attemptCliLogin({
|
||||
email: email.toLowerCase(),
|
||||
password
|
||||
password,
|
||||
captchaToken
|
||||
});
|
||||
|
||||
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
||||
@@ -83,7 +84,8 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
} else {
|
||||
const isLoginSuccessful = await attemptLogin({
|
||||
email: email.toLowerCase(),
|
||||
password
|
||||
password,
|
||||
captchaToken
|
||||
});
|
||||
|
||||
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||
@@ -117,6 +119,12 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.response.data.error === "Captcha Required") {
|
||||
setShouldShowCaptcha(true);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoginError(true);
|
||||
createNotification({
|
||||
text: "Login unsuccessful. Double-check your credentials and try again.",
|
||||
@@ -124,6 +132,11 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
});
|
||||
}
|
||||
|
||||
if (captchaRef.current) {
|
||||
captchaRef.current.resetCaptcha();
|
||||
}
|
||||
|
||||
setCaptchaToken("");
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
@@ -245,8 +258,19 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
className="select:-webkit-autofill:focus h-10"
|
||||
/>
|
||||
</div>
|
||||
{shouldShowCaptcha && (
|
||||
<div className="mt-4">
|
||||
<HCaptcha
|
||||
theme="dark"
|
||||
sitekey={CAPTCHA_SITE_KEY}
|
||||
onVerify={(token) => setCaptchaToken(token)}
|
||||
ref={captchaRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||
<Button
|
||||
disabled={shouldShowCaptcha && captchaToken === ""}
|
||||
type="submit"
|
||||
size="sm"
|
||||
isFullWidth
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
import axios from "axios";
|
||||
import jwt_decode from "jwt-decode";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
|
||||
import { Button, Input } from "@app/components/v2";
|
||||
import { useUpdateUserAuthMethods } from "@app/hooks/api";
|
||||
import { useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||
@@ -41,6 +43,10 @@ export const PasswordStep = ({
|
||||
providerAuthToken
|
||||
) as any;
|
||||
|
||||
const [captchaToken, setCaptchaToken] = useState("");
|
||||
const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false);
|
||||
const captchaRef = useRef<HCaptcha>(null);
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
@@ -51,7 +57,8 @@ export const PasswordStep = ({
|
||||
const isCliLoginSuccessful = await attemptCliLogin({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken
|
||||
providerAuthToken,
|
||||
captchaToken
|
||||
});
|
||||
|
||||
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
||||
@@ -99,7 +106,8 @@ export const PasswordStep = ({
|
||||
const loginAttempt = await attemptLogin({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken
|
||||
providerAuthToken,
|
||||
captchaToken
|
||||
});
|
||||
|
||||
if (loginAttempt && loginAttempt.success) {
|
||||
@@ -158,11 +166,21 @@ export const PasswordStep = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.response.data.error === "Captcha Required") {
|
||||
setShouldShowCaptcha(true);
|
||||
return;
|
||||
}
|
||||
|
||||
createNotification({
|
||||
text: "Login unsuccessful. Double-check your master password and try again.",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
|
||||
if (captchaRef.current) {
|
||||
captchaRef.current.resetCaptcha();
|
||||
}
|
||||
setCaptchaToken("");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -194,8 +212,19 @@ export const PasswordStep = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{shouldShowCaptcha && (
|
||||
<div className="mx-auto mt-4 flex w-full min-w-[22rem] items-center justify-center lg:w-1/6">
|
||||
<HCaptcha
|
||||
theme="dark"
|
||||
sitekey={CAPTCHA_SITE_KEY}
|
||||
onVerify={(token) => setCaptchaToken(token)}
|
||||
ref={captchaRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mx-auto mt-4 flex w-1/4 w-full min-w-[22rem] items-center justify-center rounded-md text-center lg:w-1/6">
|
||||
<Button
|
||||
disabled={shouldShowCaptcha && captchaToken === ""}
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
|
@@ -43,6 +43,7 @@ import {
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { removeTrailingSlash } from "@app/helpers/string";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
TProjectUserPrivilege,
|
||||
@@ -104,7 +105,9 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
? {
|
||||
environmentSlug: privilege.permissions?.[0]?.conditions?.environment,
|
||||
// secret path will be inside $glob operator
|
||||
secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "",
|
||||
secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob
|
||||
? removeTrailingSlash(privilege.permissions?.[0]?.conditions?.secretPath?.$glob)
|
||||
: "",
|
||||
read: privilege.permissions?.some(({ action }) =>
|
||||
action.includes(ProjectPermissionActions.Read)
|
||||
),
|
||||
@@ -183,7 +186,7 @@ export const SpecificPrivilegeSecretForm = ({
|
||||
];
|
||||
const conditions: Record<string, any> = { environment: data.environmentSlug };
|
||||
if (data.secretPath) {
|
||||
conditions.secretPath = { $glob: data.secretPath };
|
||||
conditions.secretPath = { $glob: removeTrailingSlash(data.secretPath) };
|
||||
}
|
||||
await updateUserPrivilege.mutateAsync({
|
||||
privilegeId: privilege.id,
|
||||
|
@@ -454,12 +454,12 @@ export const SecretOverviewPage = () => {
|
||||
const filteredSecretNames = secKeys
|
||||
?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase()))
|
||||
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
||||
const filteredFolderNames = folderNames?.filter((name) =>
|
||||
name.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
);
|
||||
const filteredDynamicSecrets = dynamicSecretNames?.filter((name) =>
|
||||
name.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
);
|
||||
const filteredFolderNames = folderNames
|
||||
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
|
||||
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
||||
const filteredDynamicSecrets = dynamicSecretNames
|
||||
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
|
||||
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
||||
|
||||
const isTableEmpty =
|
||||
!(
|
||||
|
@@ -178,7 +178,7 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
isVisible
|
||||
isVisible={false}
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[100px]"
|
||||
/>
|
||||
|
@@ -232,7 +232,6 @@ func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context
|
||||
}
|
||||
|
||||
managedKubeSecret.Data = plainProcessedSecrets
|
||||
managedKubeSecret.ObjectMeta.Annotations = map[string]string{}
|
||||
managedKubeSecret.ObjectMeta.Annotations[SECRET_VERSION_ANNOTATION] = ETag
|
||||
|
||||
err := r.Client.Update(ctx, &managedKubeSecret)
|
||||
|
Reference in New Issue
Block a user