Merge remote-tracking branch 'origin' into issue-cert-csr
@@ -1,178 +1,8 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { SecretType, TableName } from "../schemas";
|
||||||
import { selectAllTableCols } from "@app/lib/knex/select";
|
|
||||||
|
|
||||||
import { SecretKeyEncoding, SecretType, TableName } from "../schemas";
|
|
||||||
import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
import { getSecretManagerDataKey } from "./utils/kms";
|
|
||||||
|
|
||||||
const backfillWebhooks = async (knex: Knex) => {
|
|
||||||
const hasEncryptedSecretKeyWithKms = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms");
|
|
||||||
const hasEncryptedWebhookUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl");
|
|
||||||
const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText");
|
|
||||||
const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV");
|
|
||||||
const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag");
|
|
||||||
const hasEncryptedSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKey");
|
|
||||||
const hasIV = await knex.schema.hasColumn(TableName.Webhook, "iv");
|
|
||||||
const hasTag = await knex.schema.hasColumn(TableName.Webhook, "tag");
|
|
||||||
const hasKeyEncoding = await knex.schema.hasColumn(TableName.Webhook, "keyEncoding");
|
|
||||||
const hasAlgorithm = await knex.schema.hasColumn(TableName.Webhook, "algorithm");
|
|
||||||
const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url");
|
|
||||||
|
|
||||||
await knex.schema.alterTable(TableName.Webhook, (t) => {
|
|
||||||
if (!hasEncryptedSecretKeyWithKms) t.binary("encryptedSecretKeyWithKms");
|
|
||||||
if (!hasEncryptedWebhookUrl) t.binary("encryptedUrl");
|
|
||||||
if (hasUrl) t.string("url").nullable().alter();
|
|
||||||
});
|
|
||||||
|
|
||||||
const kmsEncryptorGroupByProjectId: Record<string, Awaited<ReturnType<typeof getSecretManagerDataKey>>["encryptor"]> =
|
|
||||||
{};
|
|
||||||
if (hasUrlCipherText && hasUrlIV && hasUrlTag && hasEncryptedSecretKey && hasIV && hasTag) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const webhooksToFill = await knex(TableName.Webhook)
|
|
||||||
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.Webhook}.envId`)
|
|
||||||
.whereNull("encryptedUrl")
|
|
||||||
// eslint-disable-next-line
|
|
||||||
// @ts-ignore knex migration fails
|
|
||||||
.select(selectAllTableCols(TableName.Webhook))
|
|
||||||
.select("projectId");
|
|
||||||
|
|
||||||
const updatedWebhooks = [];
|
|
||||||
for (const webhook of webhooksToFill) {
|
|
||||||
if (!kmsEncryptorGroupByProjectId[webhook.projectId]) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const { encryptor } = await getSecretManagerDataKey(knex, webhook.projectId);
|
|
||||||
kmsEncryptorGroupByProjectId[webhook.projectId] = encryptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
const kmsEncryptor = kmsEncryptorGroupByProjectId[webhook.projectId];
|
|
||||||
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
let webhookUrl = webhook.url;
|
|
||||||
let webhookSecretKey;
|
|
||||||
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
if (webhook.urlTag && webhook.urlCipherText && webhook.urlIV) {
|
|
||||||
webhookUrl = infisicalSymmetricDecrypt({
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
keyEncoding: webhook.keyEncoding as SecretKeyEncoding,
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
ciphertext: webhook.urlCipherText,
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
iv: webhook.urlIV,
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
tag: webhook.urlTag
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
if (webhook.encryptedSecretKey && webhook.iv && webhook.tag) {
|
|
||||||
webhookSecretKey = infisicalSymmetricDecrypt({
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
keyEncoding: webhook.keyEncoding as SecretKeyEncoding,
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
ciphertext: webhook.encryptedSecretKey,
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
iv: webhook.iv,
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
tag: webhook.tag
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const { projectId, ...el } = webhook;
|
|
||||||
updatedWebhooks.push({
|
|
||||||
...el,
|
|
||||||
encryptedSecretKeyWithKms: webhookSecretKey
|
|
||||||
? kmsEncryptor({ plainText: Buffer.from(webhookSecretKey) }).cipherTextBlob
|
|
||||||
: null,
|
|
||||||
encryptedUrl: kmsEncryptor({ plainText: Buffer.from(webhookUrl) }).cipherTextBlob
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (updatedWebhooks.length) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
await knex(TableName.Webhook).insert(updatedWebhooks).onConflict("id").merge();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await knex.schema.alterTable(TableName.Webhook, (t) => {
|
|
||||||
t.binary("encryptedUrl").notNullable().alter();
|
|
||||||
|
|
||||||
if (hasUrlIV) t.dropColumn("urlIV");
|
|
||||||
if (hasUrlCipherText) t.dropColumn("urlCipherText");
|
|
||||||
if (hasUrlTag) t.dropColumn("urlTag");
|
|
||||||
if (hasIV) t.dropColumn("iv");
|
|
||||||
if (hasTag) t.dropColumn("tag");
|
|
||||||
if (hasEncryptedSecretKey) t.dropColumn("encryptedSecretKey");
|
|
||||||
if (hasKeyEncoding) t.dropColumn("keyEncoding");
|
|
||||||
if (hasAlgorithm) t.dropColumn("algorithm");
|
|
||||||
if (hasUrl) t.dropColumn("url");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const backfillDynamicSecretConfigs = async (knex: Knex) => {
|
|
||||||
const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig");
|
|
||||||
|
|
||||||
const hasInputCipherText = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext");
|
|
||||||
const hasInputIV = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV");
|
|
||||||
const hasInputTag = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag");
|
|
||||||
const hasKeyEncoding = await knex.schema.hasColumn(TableName.DynamicSecret, "keyEncoding");
|
|
||||||
const hasAlgorithm = await knex.schema.hasColumn(TableName.DynamicSecret, "algorithm");
|
|
||||||
|
|
||||||
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
|
||||||
if (!hasEncryptedConfig) t.binary("encryptedConfig");
|
|
||||||
});
|
|
||||||
const kmsEncryptorGroupByProjectId: Record<string, Awaited<ReturnType<typeof getSecretManagerDataKey>>["encryptor"]> =
|
|
||||||
{};
|
|
||||||
if (hasInputCipherText && hasInputIV && hasInputTag) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const dynamicSecretConfigs = await knex(TableName.DynamicSecret)
|
|
||||||
.join(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
|
|
||||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
|
||||||
.whereNull("encryptedConfig")
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
.select(selectAllTableCols(TableName.DynamicSecret))
|
|
||||||
.select("projectId");
|
|
||||||
|
|
||||||
const updatedConfigs = [];
|
|
||||||
for (const dynamicSecretConfig of dynamicSecretConfigs) {
|
|
||||||
if (!kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId]) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const { encryptor } = await getSecretManagerDataKey(knex, dynamicSecretConfig.projectId);
|
|
||||||
kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId] = encryptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
const kmsEncryptor = kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId];
|
|
||||||
const inputConfig = infisicalSymmetricDecrypt({
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
keyEncoding: dynamicSecretConfig.keyEncoding as SecretKeyEncoding,
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
ciphertext: dynamicSecretConfig.inputCiphertext as string,
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
iv: dynamicSecretConfig.inputIV as string,
|
|
||||||
// @ts-ignore post migration fails
|
|
||||||
tag: dynamicSecretConfig.inputTag as string
|
|
||||||
});
|
|
||||||
|
|
||||||
const { projectId, ...el } = dynamicSecretConfig;
|
|
||||||
updatedConfigs.push({
|
|
||||||
...el,
|
|
||||||
encryptedConfig: kmsEncryptor({ plainText: Buffer.from(inputConfig) }).cipherTextBlob
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (updatedConfigs.length) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
await knex(TableName.DynamicSecret).insert(updatedConfigs).onConflict("id").merge();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
|
||||||
t.binary("encryptedConfig").notNullable().alter();
|
|
||||||
|
|
||||||
if (hasInputTag) t.dropColumn("inputTag");
|
|
||||||
if (hasInputIV) t.dropColumn("inputIV");
|
|
||||||
if (hasInputCipherText) t.dropColumn("inputCiphertext");
|
|
||||||
if (hasKeyEncoding) t.dropColumn("keyEncoding");
|
|
||||||
if (hasAlgorithm) t.dropColumn("algorithm");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
const doesSecretV2TableExist = await knex.schema.hasTable(TableName.SecretV2);
|
const doesSecretV2TableExist = await knex.schema.hasTable(TableName.SecretV2);
|
||||||
@@ -314,14 +144,6 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE");
|
t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await knex.schema.hasTable(TableName.Webhook)) {
|
|
||||||
await backfillWebhooks(knex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
|
||||||
await backfillDynamicSecretConfigs(knex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
export async function down(knex: Knex): Promise<void> {
|
||||||
@@ -356,49 +178,4 @@ export async function down(knex: Knex): Promise<void> {
|
|||||||
if (hasEncryptedAwsIamAssumRole) t.dropColumn("encryptedAwsAssumeIamRoleArn");
|
if (hasEncryptedAwsIamAssumRole) t.dropColumn("encryptedAwsAssumeIamRoleArn");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (await knex.schema.hasTable(TableName.Webhook)) {
|
|
||||||
const hasEncryptedWebhookSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms");
|
|
||||||
const hasEncryptedWebhookUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl");
|
|
||||||
const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText");
|
|
||||||
const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV");
|
|
||||||
const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag");
|
|
||||||
const hasEncryptedSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKey");
|
|
||||||
const hasIV = await knex.schema.hasColumn(TableName.Webhook, "iv");
|
|
||||||
const hasTag = await knex.schema.hasColumn(TableName.Webhook, "tag");
|
|
||||||
const hasKeyEncoding = await knex.schema.hasColumn(TableName.Webhook, "keyEncoding");
|
|
||||||
const hasAlgorithm = await knex.schema.hasColumn(TableName.Webhook, "algorithm");
|
|
||||||
const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url");
|
|
||||||
|
|
||||||
await knex.schema.alterTable(TableName.Webhook, (t) => {
|
|
||||||
if (hasEncryptedWebhookSecretKey) t.dropColumn("encryptedSecretKeyWithKms");
|
|
||||||
if (hasEncryptedWebhookUrl) t.dropColumn("encryptedUrl");
|
|
||||||
if (!hasUrl) t.string("url");
|
|
||||||
if (!hasEncryptedSecretKey) t.string("encryptedSecretKey");
|
|
||||||
if (!hasIV) t.string("iv");
|
|
||||||
if (!hasTag) t.string("tag");
|
|
||||||
if (!hasAlgorithm) t.string("algorithm");
|
|
||||||
if (!hasKeyEncoding) t.string("keyEncoding");
|
|
||||||
if (!hasUrlCipherText) t.string("urlCipherText");
|
|
||||||
if (!hasUrlIV) t.string("urlIV");
|
|
||||||
if (!hasUrlTag) t.string("urlTag");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
|
||||||
const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig");
|
|
||||||
|
|
||||||
const hasInputIV = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV");
|
|
||||||
const hasInputCipherText = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext");
|
|
||||||
const hasInputTag = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag");
|
|
||||||
const hasAlgorithm = await knex.schema.hasColumn(TableName.DynamicSecret, "algorithm");
|
|
||||||
const hasKeyEncoding = await knex.schema.hasColumn(TableName.DynamicSecret, "keyEncoding");
|
|
||||||
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
|
||||||
if (hasEncryptedConfig) t.dropColumn("encryptedConfig");
|
|
||||||
if (!hasInputIV) t.string("inputIV");
|
|
||||||
if (!hasInputCipherText) t.text("inputCiphertext");
|
|
||||||
if (!hasInputTag) t.string("inputTag");
|
|
||||||
if (!hasAlgorithm) t.string("algorithm");
|
|
||||||
if (!hasKeyEncoding) t.string("keyEncoding");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { zodBuffer } from "@app/lib/zod";
|
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const DynamicSecretsSchema = z.object({
|
export const DynamicSecretsSchema = z.object({
|
||||||
@@ -16,12 +14,16 @@ export const DynamicSecretsSchema = z.object({
|
|||||||
type: z.string(),
|
type: z.string(),
|
||||||
defaultTTL: z.string(),
|
defaultTTL: z.string(),
|
||||||
maxTTL: z.string().nullable().optional(),
|
maxTTL: z.string().nullable().optional(),
|
||||||
|
inputIV: z.string(),
|
||||||
|
inputCiphertext: z.string(),
|
||||||
|
inputTag: z.string(),
|
||||||
|
algorithm: z.string().default("aes-256-gcm"),
|
||||||
|
keyEncoding: z.string().default("utf8"),
|
||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
status: z.string().nullable().optional(),
|
status: z.string().nullable().optional(),
|
||||||
statusDetails: z.string().nullable().optional(),
|
statusDetails: z.string().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date()
|
||||||
encryptedConfig: zodBuffer
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||||
|
|||||||
@@ -5,22 +5,27 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { zodBuffer } from "@app/lib/zod";
|
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const WebhooksSchema = z.object({
|
export const WebhooksSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
secretPath: z.string().default("/"),
|
secretPath: z.string().default("/"),
|
||||||
|
url: z.string(),
|
||||||
lastStatus: z.string().nullable().optional(),
|
lastStatus: z.string().nullable().optional(),
|
||||||
lastRunErrorMessage: z.string().nullable().optional(),
|
lastRunErrorMessage: z.string().nullable().optional(),
|
||||||
isDisabled: z.boolean().default(false),
|
isDisabled: z.boolean().default(false),
|
||||||
|
encryptedSecretKey: z.string().nullable().optional(),
|
||||||
|
iv: z.string().nullable().optional(),
|
||||||
|
tag: z.string().nullable().optional(),
|
||||||
|
algorithm: z.string().nullable().optional(),
|
||||||
|
keyEncoding: z.string().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
envId: z.string().uuid(),
|
envId: z.string().uuid(),
|
||||||
type: z.string().default("general").nullable().optional(),
|
urlCipherText: z.string().nullable().optional(),
|
||||||
encryptedSecretKeyWithKms: zodBuffer.nullable().optional(),
|
urlIV: z.string().nullable().optional(),
|
||||||
encryptedUrl: zodBuffer
|
urlTag: z.string().nullable().optional(),
|
||||||
|
type: z.string().default("general").nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TWebhooks = z.infer<typeof WebhooksSchema>;
|
export type TWebhooks = z.infer<typeof WebhooksSchema>;
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
|
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
|
const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first();
|
||||||
.count("*")
|
|
||||||
.where({ dynamicSecretId })
|
|
||||||
.first();
|
|
||||||
return parseInt(doc || "0", 10);
|
return parseInt(doc || "0", 10);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
|
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
|
||||||
@@ -24,7 +21,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findById = async (id: string, tx?: Knex) => {
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
|
const doc = await (tx || db)(TableName.DynamicSecretLease)
|
||||||
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
|
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
|
||||||
.first()
|
.first()
|
||||||
.join(
|
.join(
|
||||||
@@ -40,10 +37,14 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"),
|
db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"),
|
||||||
db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"),
|
db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"),
|
||||||
db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"),
|
db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"),
|
||||||
|
db.ref("inputIV").withSchema(TableName.DynamicSecret).as("dynInputIV"),
|
||||||
|
db.ref("inputTag").withSchema(TableName.DynamicSecret).as("dynInputTag"),
|
||||||
|
db.ref("inputCiphertext").withSchema(TableName.DynamicSecret).as("dynInputCiphertext"),
|
||||||
|
db.ref("algorithm").withSchema(TableName.DynamicSecret).as("dynAlgorithm"),
|
||||||
|
db.ref("keyEncoding").withSchema(TableName.DynamicSecret).as("dynKeyEncoding"),
|
||||||
db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"),
|
db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"),
|
||||||
db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
|
db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
|
||||||
db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"),
|
db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"),
|
||||||
db.ref("encryptedConfig").withSchema(TableName.DynamicSecret).as("dynEncryptedConfig"),
|
|
||||||
db.ref("createdAt").withSchema(TableName.DynamicSecret).as("dynCreatedAt"),
|
db.ref("createdAt").withSchema(TableName.DynamicSecret).as("dynCreatedAt"),
|
||||||
db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt")
|
db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt")
|
||||||
);
|
);
|
||||||
@@ -58,12 +59,16 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
|||||||
type: doc.dynType,
|
type: doc.dynType,
|
||||||
defaultTTL: doc.dynDefaultTTL,
|
defaultTTL: doc.dynDefaultTTL,
|
||||||
maxTTL: doc.dynMaxTTL,
|
maxTTL: doc.dynMaxTTL,
|
||||||
|
inputIV: doc.dynInputIV,
|
||||||
|
inputTag: doc.dynInputTag,
|
||||||
|
inputCiphertext: doc.dynInputCiphertext,
|
||||||
|
algorithm: doc.dynAlgorithm,
|
||||||
|
keyEncoding: doc.dynKeyEncoding,
|
||||||
folderId: doc.dynFolderId,
|
folderId: doc.dynFolderId,
|
||||||
status: doc.dynStatus,
|
status: doc.dynStatus,
|
||||||
statusDetails: doc.dynStatusDetails,
|
statusDetails: doc.dynStatusDetails,
|
||||||
createdAt: doc.dynCreatedAt,
|
createdAt: doc.dynCreatedAt,
|
||||||
updatedAt: doc.dynUpdatedAt,
|
updatedAt: doc.dynUpdatedAt
|
||||||
encryptedConfig: doc.dynEncryptedConfig
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
||||||
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
|
||||||
|
|
||||||
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
||||||
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
||||||
@@ -15,8 +14,6 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
|||||||
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
|
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
|
||||||
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
|
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
|
||||||
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
|
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
|
||||||
@@ -25,9 +22,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
queueService,
|
queueService,
|
||||||
dynamicSecretDAL,
|
dynamicSecretDAL,
|
||||||
dynamicSecretProviders,
|
dynamicSecretProviders,
|
||||||
dynamicSecretLeaseDAL,
|
dynamicSecretLeaseDAL
|
||||||
kmsService,
|
|
||||||
folderDAL
|
|
||||||
}: TDynamicSecretLeaseQueueServiceFactoryDep) => {
|
}: TDynamicSecretLeaseQueueServiceFactoryDep) => {
|
||||||
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
|
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
|
||||||
await queueService.queue(
|
await queueService.queue(
|
||||||
@@ -82,20 +77,15 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
||||||
|
|
||||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||||
const folder = await folderDAL.findById(dynamicSecretCfg.folderId);
|
|
||||||
if (!folder) throw new DisableRotationErrors({ message: "Folder not found" });
|
|
||||||
const { projectId } = folder;
|
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
|
||||||
type: KmsDataKey.SecretManager,
|
|
||||||
projectId
|
|
||||||
});
|
|
||||||
|
|
||||||
const dynamicSecretInputConfig = secretManagerDecryptor({
|
|
||||||
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
|
||||||
}).toString();
|
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
const decryptedStoredInput = JSON.parse(
|
||||||
|
infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
|
tag: dynamicSecretCfg.inputTag,
|
||||||
|
iv: dynamicSecretCfg.inputIV
|
||||||
|
})
|
||||||
|
) as object;
|
||||||
|
|
||||||
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId);
|
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId);
|
||||||
await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
|
await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
|
||||||
@@ -110,22 +100,17 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
|
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
|
||||||
throw new DisableRotationErrors({ message: "Document not deleted" });
|
throw new DisableRotationErrors({ message: "Document not deleted" });
|
||||||
|
|
||||||
const folder = await folderDAL.findById(dynamicSecretCfg.folderId);
|
|
||||||
if (!folder) throw new DisableRotationErrors({ message: "Folder not found" });
|
|
||||||
const { projectId } = folder;
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
|
||||||
type: KmsDataKey.SecretManager,
|
|
||||||
projectId
|
|
||||||
});
|
|
||||||
|
|
||||||
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId });
|
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId });
|
||||||
if (dynamicSecretLeases.length) {
|
if (dynamicSecretLeases.length) {
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
|
const decryptedStoredInput = JSON.parse(
|
||||||
const dynamicSecretInputConfig = secretManagerDecryptor({
|
infisicalSymmetricDecrypt({
|
||||||
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
}).toString();
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
tag: dynamicSecretCfg.inputTag,
|
||||||
|
iv: dynamicSecretCfg.inputIV
|
||||||
|
})
|
||||||
|
) as object;
|
||||||
|
|
||||||
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
|
||||||
@@ -34,7 +34,6 @@ type TDynamicSecretLeaseServiceFactoryDep = {
|
|||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
|
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
|
||||||
@@ -47,8 +46,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
dynamicSecretQueueService,
|
dynamicSecretQueueService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
licenseService,
|
licenseService
|
||||||
kmsService
|
|
||||||
}: TDynamicSecretLeaseServiceFactoryDep) => {
|
}: TDynamicSecretLeaseServiceFactoryDep) => {
|
||||||
const create = async ({
|
const create = async ({
|
||||||
environmentSlug,
|
environmentSlug,
|
||||||
@@ -96,12 +94,14 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
|
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
|
||||||
|
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const decryptedStoredInput = JSON.parse(
|
||||||
type: KmsDataKey.SecretManager,
|
infisicalSymmetricDecrypt({
|
||||||
projectId
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
});
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
|
tag: dynamicSecretCfg.inputTag,
|
||||||
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
|
iv: dynamicSecretCfg.inputIV
|
||||||
|
})
|
||||||
|
) as object;
|
||||||
|
|
||||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
||||||
const { maxTTL } = dynamicSecretCfg;
|
const { maxTTL } = dynamicSecretCfg;
|
||||||
@@ -164,12 +164,14 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
|
|
||||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const decryptedStoredInput = JSON.parse(
|
||||||
type: KmsDataKey.SecretManager,
|
infisicalSymmetricDecrypt({
|
||||||
projectId
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
});
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
|
tag: dynamicSecretCfg.inputTag,
|
||||||
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
|
iv: dynamicSecretCfg.inputIV
|
||||||
|
})
|
||||||
|
) as object;
|
||||||
|
|
||||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
||||||
const { maxTTL } = dynamicSecretCfg;
|
const { maxTTL } = dynamicSecretCfg;
|
||||||
@@ -229,12 +231,14 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
|
|
||||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const decryptedStoredInput = JSON.parse(
|
||||||
type: KmsDataKey.SecretManager,
|
infisicalSymmetricDecrypt({
|
||||||
projectId
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
});
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
|
tag: dynamicSecretCfg.inputTag,
|
||||||
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
|
iv: dynamicSecretCfg.inputIV
|
||||||
|
})
|
||||||
|
) as object;
|
||||||
|
|
||||||
const revokeResponse = await selectedProvider
|
const revokeResponse = await selectedProvider
|
||||||
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)
|
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
|
||||||
@@ -34,7 +34,6 @@ type TDynamicSecretServiceFactoryDep = {
|
|||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||||
@@ -47,8 +46,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
dynamicSecretProviders,
|
dynamicSecretProviders,
|
||||||
permissionService,
|
permissionService,
|
||||||
dynamicSecretQueueService,
|
dynamicSecretQueueService,
|
||||||
projectDAL,
|
projectDAL
|
||||||
kmsService
|
|
||||||
}: TDynamicSecretServiceFactoryDep) => {
|
}: TDynamicSecretServiceFactoryDep) => {
|
||||||
const create = async ({
|
const create = async ({
|
||||||
path,
|
path,
|
||||||
@@ -98,16 +96,16 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
|
|
||||||
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
|
||||||
type: KmsDataKey.SecretManager,
|
|
||||||
projectId
|
|
||||||
});
|
|
||||||
|
|
||||||
const encryptedConfig = secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob;
|
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
||||||
type: provider.type,
|
type: provider.type,
|
||||||
version: 1,
|
version: 1,
|
||||||
encryptedConfig,
|
inputIV: encryptedInput.iv,
|
||||||
|
inputTag: encryptedInput.tag,
|
||||||
|
inputCiphertext: encryptedInput.ciphertext,
|
||||||
|
algorithm: encryptedInput.algorithm,
|
||||||
|
keyEncoding: encryptedInput.encoding,
|
||||||
maxTTL,
|
maxTTL,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
@@ -167,28 +165,27 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
|
const decryptedStoredInput = JSON.parse(
|
||||||
await kmsService.createCipherPairWithDataKey({
|
infisicalSymmetricDecrypt({
|
||||||
type: KmsDataKey.SecretManager,
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
projectId
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
});
|
tag: dynamicSecretCfg.inputTag,
|
||||||
const dynamicSecretInputConfig = secretManagerDecryptor({
|
iv: dynamicSecretCfg.inputIV
|
||||||
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
})
|
||||||
}).toString();
|
) as object;
|
||||||
|
|
||||||
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
|
||||||
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
||||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
||||||
|
|
||||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
|
||||||
const encryptedConfig = secretManagerEncryptor({
|
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput));
|
||||||
plainText: Buffer.from(JSON.stringify(updatedInput))
|
|
||||||
}).cipherTextBlob;
|
|
||||||
|
|
||||||
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
||||||
encryptedConfig,
|
inputIV: encryptedInput.iv,
|
||||||
|
inputTag: encryptedInput.tag,
|
||||||
|
inputCiphertext: encryptedInput.ciphertext,
|
||||||
|
algorithm: encryptedInput.algorithm,
|
||||||
|
keyEncoding: encryptedInput.encoding,
|
||||||
maxTTL,
|
maxTTL,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
name: newName ?? name,
|
name: newName ?? name,
|
||||||
@@ -289,16 +286,14 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const decryptedStoredInput = JSON.parse(
|
||||||
type: KmsDataKey.SecretManager,
|
infisicalSymmetricDecrypt({
|
||||||
projectId
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
});
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
|
tag: dynamicSecretCfg.inputTag,
|
||||||
const dynamicSecretInputConfig = secretManagerDecryptor({
|
iv: dynamicSecretCfg.inputIV
|
||||||
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
})
|
||||||
}).toString();
|
) as object;
|
||||||
|
|
||||||
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
||||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export enum ProjectPermissionSub {
|
|||||||
IpAllowList = "ip-allowlist",
|
IpAllowList = "ip-allowlist",
|
||||||
Project = "workspace",
|
Project = "workspace",
|
||||||
Secrets = "secrets",
|
Secrets = "secrets",
|
||||||
|
SecretFolders = "secret-folders",
|
||||||
SecretRollback = "secret-rollback",
|
SecretRollback = "secret-rollback",
|
||||||
SecretApproval = "secret-approval",
|
SecretApproval = "secret-approval",
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
@@ -42,6 +43,10 @@ export type ProjectPermissionSet =
|
|||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
||||||
]
|
]
|
||||||
|
| [
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
|
||||||
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||||
|
|||||||
@@ -635,7 +635,8 @@ export const registerRoutes = async (
|
|||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
kmsService
|
kmsService,
|
||||||
|
projectBotDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectEnvService = projectEnvServiceFactory({
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
@@ -677,8 +678,7 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
webhookDAL,
|
webhookDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectDAL,
|
projectDAL
|
||||||
kmsService
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService });
|
const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService });
|
||||||
@@ -988,9 +988,7 @@ export const registerRoutes = async (
|
|||||||
queueService,
|
queueService,
|
||||||
dynamicSecretLeaseDAL,
|
dynamicSecretLeaseDAL,
|
||||||
dynamicSecretProviders,
|
dynamicSecretProviders,
|
||||||
dynamicSecretDAL,
|
dynamicSecretDAL
|
||||||
kmsService,
|
|
||||||
folderDAL
|
|
||||||
});
|
});
|
||||||
const dynamicSecretService = dynamicSecretServiceFactory({
|
const dynamicSecretService = dynamicSecretServiceFactory({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -1000,8 +998,7 @@ export const registerRoutes = async (
|
|||||||
dynamicSecretProviders,
|
dynamicSecretProviders,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService
|
||||||
kmsService
|
|
||||||
});
|
});
|
||||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -1011,8 +1008,7 @@ export const registerRoutes = async (
|
|||||||
dynamicSecretLeaseDAL,
|
dynamicSecretLeaseDAL,
|
||||||
dynamicSecretProviders,
|
dynamicSecretProviders,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
licenseService,
|
licenseService
|
||||||
kmsService
|
|
||||||
});
|
});
|
||||||
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
|
|||||||
@@ -129,7 +129,11 @@ export const SanitizedRoleSchema = ProjectRolesSchema.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
||||||
encryptedConfig: true
|
inputIV: true,
|
||||||
|
inputTag: true,
|
||||||
|
inputCiphertext: true,
|
||||||
|
keyEncoding: true,
|
||||||
|
algorithm: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SanitizedAuditLogStreamSchema = z.object({
|
export const SanitizedAuditLogStreamSchema = z.object({
|
||||||
|
|||||||
@@ -540,7 +540,7 @@ export const projectMembershipServiceFactory = ({
|
|||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
|
||||||
if (project.version !== ProjectVersion.V2) {
|
if (project.version === ProjectVersion.V1) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Please ask your project administrator to upgrade the project before leaving."
|
message: "Please ask your project administrator to upgrade the project before leaving."
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id
|
|||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TOrgServiceFactory } from "../org/org-service";
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
@@ -74,6 +75,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||||
|
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
||||||
kmsService: Pick<
|
kmsService: Pick<
|
||||||
TKmsServiceFactory,
|
TKmsServiceFactory,
|
||||||
| "updateProjectSecretManagerKmsKey"
|
| "updateProjectSecretManagerKmsKey"
|
||||||
@@ -106,7 +108,8 @@ export const projectServiceFactory = ({
|
|||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
kmsService
|
kmsService,
|
||||||
|
projectBotDAL
|
||||||
}: TProjectServiceFactoryDep) => {
|
}: TProjectServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Create workspace. Make user the admin
|
* Create workspace. Make user the admin
|
||||||
@@ -206,7 +209,26 @@ export const projectServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
// const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
|
const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey);
|
||||||
|
|
||||||
|
// 5. Create & a bot for the project
|
||||||
|
await projectBotDAL.create(
|
||||||
|
{
|
||||||
|
name: "Infisical Bot (Ghost)",
|
||||||
|
projectId: project.id,
|
||||||
|
tag,
|
||||||
|
iv,
|
||||||
|
encryptedProjectKey,
|
||||||
|
encryptedProjectKeyNonce: encryptedProjectKeyIv,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
isActive: true,
|
||||||
|
publicKey: ghostUser.keys.publicKey,
|
||||||
|
senderId: ghostUser.user.id,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding: encoding
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
// Find the ghost users latest key
|
// Find the ghost users latest key
|
||||||
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
|
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { RawRule } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
|
||||||
|
export const shouldCheckFolderPermission = (rules: RawRule[]) =>
|
||||||
|
rules.some((rule) => (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders));
|
||||||
@@ -11,6 +11,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
||||||
|
import { shouldCheckFolderPermission } from "./secret-folder-fns";
|
||||||
import {
|
import {
|
||||||
TCreateFolderDTO,
|
TCreateFolderDTO,
|
||||||
TDeleteFolderDTO,
|
TDeleteFolderDTO,
|
||||||
@@ -57,10 +58,21 @@ export const secretFolderServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
// we do this because we've split Secret and SecretFolder resources
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
// previously, if one can create/update/read/delete secrets then they can do the same for folders
|
||||||
);
|
// for backwards compatibility, we handle authorization only when SecretFolders subject is used
|
||||||
|
if (shouldCheckFolderPermission(permission.rules)) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
||||||
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||||
@@ -148,10 +160,20 @@ export const secretFolderServiceFactory = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
folders.forEach(({ environment, path: secretPath }) => {
|
folders.forEach(({ environment, path: secretPath }) => {
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
// we do this because we've split Secret and SecretFolder resources
|
||||||
ProjectPermissionActions.Edit,
|
// previously, if one can create/update/read/delete secrets then they can do the same for folders
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
// for backwards compatibility, we handle authorization only when SecretFolders subject is used
|
||||||
);
|
if (shouldCheckFolderPermission(permission.rules)) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await folderDAL.transaction(async (tx) =>
|
const result = await folderDAL.transaction(async (tx) =>
|
||||||
@@ -243,10 +265,21 @@ export const secretFolderServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
// we do this because we've split Secret and SecretFolder resources
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
// previously, if one can create/update/read/delete secrets then they can do the same for folders
|
||||||
);
|
// for backwards compatibility, we handle authorization differently only when SecretFolders subject is used
|
||||||
|
if (shouldCheckFolderPermission(permission.rules)) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
|
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
|
||||||
@@ -316,10 +349,21 @@ export const secretFolderServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
// we do this because we've split Secret and SecretFolder resources
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
// previously, if one can create/update/read/delete secrets then they can do the same for folders
|
||||||
);
|
// for backwards compatibility, we handle authorization differently only when SecretFolders subject is used
|
||||||
|
if (shouldCheckFolderPermission(permission.rules)) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
||||||
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||||
|
|||||||
@@ -1133,7 +1133,7 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
queueService.start(QueueName.SecretWebhook, async (job) => {
|
queueService.start(QueueName.SecretWebhook, async (job) => {
|
||||||
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL, kmsService });
|
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL });
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import crypto from "node:crypto";
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
|
import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TWebhookDALFactory } from "./webhook-dal";
|
import { TWebhookDALFactory } from "./webhook-dal";
|
||||||
@@ -16,12 +16,40 @@ import { WebhookType } from "./webhook-types";
|
|||||||
|
|
||||||
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
|
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
|
||||||
|
|
||||||
export const triggerWebhookRequest = async (
|
export const decryptWebhookDetails = (webhook: TWebhooks) => {
|
||||||
{ webhookSecretKey: secretKey, webhookUrl: url }: { webhookSecretKey?: string; webhookUrl: string },
|
const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook;
|
||||||
data: Record<string, unknown>
|
|
||||||
) => {
|
let decryptedSecretKey = "";
|
||||||
|
let decryptedUrl = url;
|
||||||
|
|
||||||
|
if (encryptedSecretKey) {
|
||||||
|
decryptedSecretKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: encryptedSecretKey,
|
||||||
|
iv: iv as string,
|
||||||
|
tag: tag as string
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlCipherText) {
|
||||||
|
decryptedUrl = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: urlCipherText,
|
||||||
|
iv: urlIV as string,
|
||||||
|
tag: urlTag as string
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
secretKey: decryptedSecretKey,
|
||||||
|
url: decryptedUrl
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record<string, unknown>) => {
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
const payload = { ...data, timestamp: Date.now() };
|
const payload = { ...data, timestamp: Date.now() };
|
||||||
|
const { secretKey, url } = decryptWebhookDetails(webhook);
|
||||||
|
|
||||||
if (secretKey) {
|
if (secretKey) {
|
||||||
const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex");
|
const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex");
|
||||||
@@ -96,7 +124,6 @@ export type TFnTriggerWebhookDTO = {
|
|||||||
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// this is reusable function
|
// this is reusable function
|
||||||
@@ -107,8 +134,7 @@ export const fnTriggerWebhook = async ({
|
|||||||
projectId,
|
projectId,
|
||||||
webhookDAL,
|
webhookDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectDAL,
|
projectDAL
|
||||||
kmsService
|
|
||||||
}: TFnTriggerWebhookDTO) => {
|
}: TFnTriggerWebhookDTO) => {
|
||||||
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
|
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
|
||||||
const toBeTriggeredHooks = webhooks.filter(
|
const toBeTriggeredHooks = webhooks.filter(
|
||||||
@@ -118,20 +144,10 @@ export const fnTriggerWebhook = async ({
|
|||||||
if (!toBeTriggeredHooks.length) return;
|
if (!toBeTriggeredHooks.length) return;
|
||||||
logger.info("Secret webhook job started", { environment, secretPath, projectId });
|
logger.info("Secret webhook job started", { environment, secretPath, projectId });
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
|
||||||
projectId,
|
|
||||||
type: KmsDataKey.SecretManager
|
|
||||||
});
|
|
||||||
|
|
||||||
const webhooksTriggered = await Promise.allSettled(
|
const webhooksTriggered = await Promise.allSettled(
|
||||||
toBeTriggeredHooks.map((hook) => {
|
toBeTriggeredHooks.map((hook) =>
|
||||||
const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedUrl }).toString();
|
triggerWebhookRequest(
|
||||||
const webhookSecretKey = hook.encryptedSecretKeyWithKms
|
hook,
|
||||||
? kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedSecretKeyWithKms }).toString()
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return triggerWebhookRequest(
|
|
||||||
{ webhookUrl, webhookSecretKey },
|
|
||||||
getWebhookPayload("secrets.modified", {
|
getWebhookPayload("secrets.modified", {
|
||||||
workspaceName: project.name,
|
workspaceName: project.name,
|
||||||
workspaceId: projectId,
|
workspaceId: projectId,
|
||||||
@@ -139,8 +155,8 @@ export const fnTriggerWebhook = async ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
type: hook.type
|
type: hook.type
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
})
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// filter hooks by status
|
// filter hooks by status
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { TWebhooksInsert } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TWebhookDALFactory } from "./webhook-dal";
|
import { TWebhookDALFactory } from "./webhook-dal";
|
||||||
import { getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
|
import { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
|
||||||
import {
|
import {
|
||||||
TCreateWebhookDTO,
|
TCreateWebhookDTO,
|
||||||
TDeleteWebhookDTO,
|
TDeleteWebhookDTO,
|
||||||
@@ -23,7 +23,6 @@ type TWebhookServiceFactoryDep = {
|
|||||||
projectEnvDAL: TProjectEnvDALFactory;
|
projectEnvDAL: TProjectEnvDALFactory;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
|
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
|
||||||
@@ -32,8 +31,7 @@ export const webhookServiceFactory = ({
|
|||||||
webhookDAL,
|
webhookDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL
|
||||||
kmsService
|
|
||||||
}: TWebhookServiceFactoryDep) => {
|
}: TWebhookServiceFactoryDep) => {
|
||||||
const createWebhook = async ({
|
const createWebhook = async ({
|
||||||
actor,
|
actor,
|
||||||
@@ -58,28 +56,33 @@ export const webhookServiceFactory = ({
|
|||||||
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
||||||
if (!env) throw new BadRequestError({ message: "Env not found" });
|
if (!env) throw new BadRequestError({ message: "Env not found" });
|
||||||
|
|
||||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
const insertDoc: TWebhooksInsert = {
|
||||||
projectId,
|
url: "", // deprecated - we are moving away from plaintext URLs
|
||||||
type: KmsDataKey.SecretManager
|
|
||||||
});
|
|
||||||
|
|
||||||
const encryptedSecretKeyWithKms = webhookSecretKey
|
|
||||||
? secretManagerEncryptor({
|
|
||||||
plainText: Buffer.from(webhookSecretKey)
|
|
||||||
}).cipherTextBlob
|
|
||||||
: null;
|
|
||||||
const encryptedUrl = secretManagerEncryptor({
|
|
||||||
plainText: Buffer.from(webhookUrl)
|
|
||||||
}).cipherTextBlob;
|
|
||||||
|
|
||||||
const webhook = await webhookDAL.create({
|
|
||||||
encryptedUrl,
|
|
||||||
encryptedSecretKeyWithKms,
|
|
||||||
envId: env.id,
|
envId: env.id,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
secretPath: secretPath || "/",
|
secretPath: secretPath || "/",
|
||||||
type
|
type
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (webhookSecretKey) {
|
||||||
|
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey);
|
||||||
|
insertDoc.encryptedSecretKey = ciphertext;
|
||||||
|
insertDoc.iv = iv;
|
||||||
|
insertDoc.tag = tag;
|
||||||
|
insertDoc.algorithm = algorithm;
|
||||||
|
insertDoc.keyEncoding = encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webhookUrl) {
|
||||||
|
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookUrl);
|
||||||
|
insertDoc.urlCipherText = ciphertext;
|
||||||
|
insertDoc.urlIV = iv;
|
||||||
|
insertDoc.urlTag = tag;
|
||||||
|
insertDoc.algorithm = algorithm;
|
||||||
|
insertDoc.keyEncoding = encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhook = await webhookDAL.create(insertDoc);
|
||||||
return { ...webhook, projectId, environment: env };
|
return { ...webhook, projectId, environment: env };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,18 +136,9 @@ export const webhookServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||||
let webhookError: string | undefined;
|
let webhookError: string | undefined;
|
||||||
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
|
||||||
projectId: project.id,
|
|
||||||
type: KmsDataKey.SecretManager
|
|
||||||
});
|
|
||||||
const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedUrl }).toString();
|
|
||||||
const webhookSecretKey = webhook.encryptedSecretKeyWithKms
|
|
||||||
? kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedSecretKeyWithKms }).toString()
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await triggerWebhookRequest(
|
await triggerWebhookRequest(
|
||||||
{ webhookUrl, webhookSecretKey },
|
webhook,
|
||||||
getWebhookPayload("test", {
|
getWebhookPayload("test", {
|
||||||
workspaceName: project.name,
|
workspaceName: project.name,
|
||||||
workspaceId: webhook.projectId,
|
workspaceId: webhook.projectId,
|
||||||
@@ -183,15 +177,11 @@ export const webhookServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||||
|
|
||||||
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath);
|
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath);
|
||||||
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
|
||||||
type: KmsDataKey.SecretManager,
|
|
||||||
projectId
|
|
||||||
});
|
|
||||||
return webhooks.map((w) => {
|
return webhooks.map((w) => {
|
||||||
const decryptedUrl = kmsDataKeyDecryptor({ cipherTextBlob: w.encryptedUrl }).toString();
|
const { url } = decryptWebhookDetails(w);
|
||||||
return {
|
return {
|
||||||
...w,
|
...w,
|
||||||
url: decryptedUrl
|
url
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
title: "AWS CloudHSM"
|
||||||
|
description: "Learn how to manage encryption using AWS CloudHSM"
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide provides instructions on securing Infisical project secrets using AWS CloudHSM.
|
||||||
|
Integration with AWS CloudHSM is achieved by configuring it as a custom key store for AWS KMS.
|
||||||
|
Follow the steps below to set up AWS KMS with AWS CloudHSM as the custom key store.
|
||||||
|
|
||||||
|
## Prepare AWS CloudHSM Cluster
|
||||||
|
|
||||||
|
Before you get started, you'll need to configure a AWS CloudHSM cluster which meets the following criteria:
|
||||||
|
|
||||||
|
- The cluster must be active.
|
||||||
|
- The cluster must not be associated with any other AWS KMS custom key store.
|
||||||
|
- The cluster must be configured with private subnets in at least two Availability Zones in the Region.
|
||||||
|
- The security group for the cluster must include inbound and outbound rules that allow TCP traffic on ports 2223-2225.
|
||||||
|
- The cluster must contain at least two active HSMs in different Availability Zones.
|
||||||
|
|
||||||
|
For more details on setting up your cluster, refer to the following [AWS documentation](https://docs.aws.amazon.com/kms/latest/developerguide/create-keystore.html#before-keystore).
|
||||||
|
|
||||||
|
## Set Up AWS KMS Custom Key Store
|
||||||
|
|
||||||
|
To set up an AWS KMS custom key store with AWS CloudHSM, you will need the following:
|
||||||
|
|
||||||
|
- The trust anchor certificate of your AWS CloudHSM cluster.
|
||||||
|
- A `kmsuser` user in the AWS CloudHSM cluster with the crypto-user role.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to Key store creation page">
|
||||||
|
In the AWS console, head over to `AWS KMS` > `AWS CloudHSM key stores` and click **Create key store**.
|
||||||
|
</Step>
|
||||||
|
<Step title="Add key store name">
|
||||||
|
Input the custom key store name. 
|
||||||
|
</Step>
|
||||||
|
<Step title="Select HSM cluster">
|
||||||
|
Select the AWS CloudHSM cluster. You should be able to select the cluster if it meets the required criteria mentioned above.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Upload trust anchor certificate">
|
||||||
|
Upload your CloudHSM's cluster trust anchor certificate file.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Provide cluster user password">
|
||||||
|
Input the password of the `kmsuser` crypto-user in your cluster.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Finish key store creation">
|
||||||
|
Proceed with creating the AWS CloudHSM key store.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
For more details, refer to the following [AWS documentation](https://docs.aws.amazon.com/kms/latest/developerguide/create-keystore.html#create-keystore-console).
|
||||||
|
|
||||||
|
## Create AWS KMS Key
|
||||||
|
Next, you'll need to create a AWS KMS key where you will set the key store you created previously.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to AWS KMS key creation page">
|
||||||
|
In your AWS console, proceed to `AWS KMS` > `Customer managed keys` and click **Create**.
|
||||||
|
</Step>
|
||||||
|
<Step title="Set key options">
|
||||||
|
Set Key type to `Symmetric` and Key usage to `Encrypt and decrypt`.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select key material origin">
|
||||||
|
In the advanced options, for the Key material origin field, select `AWS CloudHSM key store`. Then, click next.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Choose key store">
|
||||||
|
Select the AWS CloudHSM key store you created earlier.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Finish KMS key creation">
|
||||||
|
Proceed with creating the AWS KMS Key.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Connect Infisical to AWS KMS Key
|
||||||
|
|
||||||
|
You should now have an AWS KMS that has a custom key store set to AWS CloudHSM.
|
||||||
|
To secure project resources, you will need to add this AWS KMS to your Infisical organization. To learn how, refer to the documentation [here](./aws-kms).
|
||||||
@@ -1,24 +1,26 @@
|
|||||||
---
|
---
|
||||||
title: "AWS Key Management Service (KMS)"
|
title: "AWS Key Management Service"
|
||||||
description: "Learn how to manage encryption using AWS KMS"
|
description: "Learn how to manage encryption using AWS KMS"
|
||||||
---
|
---
|
||||||
|
|
||||||
You can configure your projects to use AWS KMS keys for encryption, enhancing the security and management of your secrets.
|
To enhance the security of your Infisical projects, you can now encrypt your secrets using an external Key Management Service (KMS).
|
||||||
|
When external KMS is configured for your project, all encryption and decryption operations will be handled by the chosen KMS.
|
||||||
|
This guide will walk you through the steps needed to configure external KMS support with AWS KMS.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Depending on the AWS Authentication Method you intend to use, you will have to do either of the following:
|
Before you begin, you'll first need to choose a method of authentication with AWS from below.
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Assume Role (Recommended)">
|
<Tab title="Method 1: Assume Role (Recommended)">
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Create the Managing User IAM Role">
|
<Step title="Create the Managing User IAM Role">
|
||||||
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
||||||

|

|
||||||
|
|
||||||
2. Select **AWS Account** as the **Trusted Entity Type**.
|
2. Select **AWS Account** as the **Trusted Entity Type**.
|
||||||
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
|
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted.
|
||||||
4. Optionally, enable **Require external ID** and enter your **project ID** to further enhance security.
|
4. Optionally, enable **Require external ID** and enter your Infisical **project ID** to further enhance security.
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Add Required Permissions for the IAM Role">
|
<Step title="Add Required Permissions for the IAM Role">
|
||||||
Use the following custom policy to grant the minimum permissions required by Infisical to integrate with AWS KMS
|
Use the following custom policy to grant the minimum permissions required by Infisical to integrate with AWS KMS
|
||||||
@@ -44,7 +46,7 @@ Depending on the AWS Authentication Method you intend to use, you will have to d
|
|||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Access Key">
|
<Tab title="Method 2: Access Key">
|
||||||
Navigate to your IAM user and add a policy to grant the following permissions:
|
Navigate to your IAM user and add a policy to grant the following permissions:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -68,10 +70,10 @@ Depending on the AWS Authentication Method you intend to use, you will have to d
|
|||||||
|
|
||||||
## Setup AWS KMS in the Organization Settings
|
## Setup AWS KMS in the Organization Settings
|
||||||
|
|
||||||
Follow these steps to set up AWS KMS for your organization:
|
Next, you will need to follow the steps listed below to add AWS KMS for your organization.
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Navigate to the organization settings and select the Encryption tab.">
|
<Step title="Navigate to the organization settings and select the 'Encryption' tab.">
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Click on the 'Add' button">
|
<Step title="Click on the 'Add' button">
|
||||||
@@ -83,7 +85,8 @@ Follow these steps to set up AWS KMS for your organization:
|
|||||||
Choose 'AWS KMS' from the list of encryption providers.
|
Choose 'AWS KMS' from the list of encryption providers.
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Provide the inputs for AWS KMS">
|
<Step title="Provide the inputs for AWS KMS">
|
||||||
Fill in the required details for AWS KMS:
|
Selecting AWS as the provider will require you input the following fields.
|
||||||
|
|
||||||
<ParamField path="Alias" type="string" required>
|
<ParamField path="Alias" type="string" required>
|
||||||
Name for referencing the AWS KMS key within the organization.
|
Name for referencing the AWS KMS key within the organization.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
@@ -96,7 +99,7 @@ Follow these steps to set up AWS KMS for your organization:
|
|||||||
Authentication mode for AWS, either "AWS Assume Role" or "Access Key".
|
Authentication mode for AWS, either "AWS Assume Role" or "Access Key".
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="IAM Role ARN For Role Assumption" type="string" required>
|
<ParamField path="IAM Role ARN For Role Assumption" type="string">
|
||||||
ARN of the AWS role to assume for providing Infisical access to the AWS KMS Key (required if Authentication Mode is "AWS Assume Role")
|
ARN of the AWS role to assume for providing Infisical access to the AWS KMS Key (required if Authentication Mode is "AWS Assume Role")
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
@@ -104,11 +107,11 @@ Follow these steps to set up AWS KMS for your organization:
|
|||||||
Custom identifier for additional validation during role assumption.
|
Custom identifier for additional validation during role assumption.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Access Key ID" type="string" required>
|
<ParamField path="Access Key ID" type="string">
|
||||||
AWS IAM Access Key ID for authentication (required if Authentication Mode is "Access Key").
|
AWS IAM Access Key ID for authentication (required if Authentication Mode is "Access Key").
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Secret Access Key" type="string" required>
|
<ParamField path="Secret Access Key" type="string">
|
||||||
AWS IAM Secret Access Key for authentication (required if Authentication Mode is "Access Key").
|
AWS IAM Secret Access Key for authentication (required if Authentication Mode is "Access Key").
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
@@ -126,14 +129,14 @@ Follow these steps to set up AWS KMS for your organization:
|
|||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
You now have an AWS KMS Key configured at the organization level. You can assign these keys to existing projects via the Project Settings page.
|
You now have an AWS KMS Key configured at the organization level. You can assign these AWS KMS keys to existing Infisical projects by visiting the 'Project Settings' page.
|
||||||
|
|
||||||
## Assign AWS KMS Key to an Existing Project
|
## Assign AWS KMS Key to an Existing Project
|
||||||
|
|
||||||
Follow these steps to assign an AWS KMS key to a project:
|
To assign the AWS KMS key you added to your organization, follow the steps below.
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Open Project Settings and proceed to the Encryption Tab">
|
<Step title="Open Project Settings and select to the Encryption Tab">
|
||||||

|
settings](../../../images/platform/kms/aws/encryption-project-settings.png)
|
||||||
</Step>
|
</Step>
|
||||||
@@ -143,6 +146,6 @@ Follow these steps to assign an AWS KMS key to a project:
|
|||||||
Choose the AWS KMS key you configured earlier.
|
Choose the AWS KMS key you configured earlier.
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Click Save">
|
<Step title="Click Save">
|
||||||
Save the changes to apply the new encryption settings to your project.
|
Once you have selected the KMS of choice, click save.
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 134 KiB |
@@ -158,7 +158,8 @@
|
|||||||
"group": "Key Management",
|
"group": "Key Management",
|
||||||
"pages": [
|
"pages": [
|
||||||
"documentation/platform/kms/overview",
|
"documentation/platform/kms/overview",
|
||||||
"documentation/platform/kms/aws-kms"
|
"documentation/platform/kms/aws-kms",
|
||||||
|
"documentation/platform/kms/aws-hsm"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"documentation/platform/secret-sharing"
|
"documentation/platform/secret-sharing"
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import { Spinner } from "@app/components/v2";
|
|
||||||
import { useWorkspace } from "@app/context";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
import { useGetUpgradeProjectStatus } from "@app/hooks/api/workspace/queries";
|
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
|
||||||
|
|
||||||
export const UpgradeOverlay = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const [isUpgrading, setIsUpgrading] = useToggle(false);
|
|
||||||
|
|
||||||
const isProjectRoute = router.pathname.includes("/project");
|
|
||||||
|
|
||||||
const { isLoading: isUpgradeStatusLoading } = useGetUpgradeProjectStatus({
|
|
||||||
projectId: currentWorkspace?.id ?? "",
|
|
||||||
enabled: isProjectRoute && currentWorkspace && currentWorkspace.version === ProjectVersion.V1,
|
|
||||||
refetchInterval: 5_000,
|
|
||||||
onSuccess: (data) => {
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
if (data.status !== "IN_PROGRESS") {
|
|
||||||
setIsUpgrading.off();
|
|
||||||
} else if (data?.status === "IN_PROGRESS") {
|
|
||||||
setIsUpgrading.on();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// make sure only to display this on /project routes
|
|
||||||
if (!currentWorkspace || !isProjectRoute) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !isUpgradeStatusLoading && isUpgrading ? (
|
|
||||||
<div className="absolute top-0 left-0 z-50 flex h-screen w-screen items-center justify-center bg-bunker-500 bg-opacity-80">
|
|
||||||
<Spinner size="lg" className="text-primary" />
|
|
||||||
<div className="ml-4 flex flex-col space-y-1">
|
|
||||||
<div className="text-3xl font-medium text-white">Please wait</div>
|
|
||||||
<span className="inline-block text-white">Upgrading your project...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { UpgradeOverlay } from "./UpgradeOverlay";
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { useCallback, useState } from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { useProjectPermission } from "@app/context";
|
|
||||||
import { useGetUpgradeProjectStatus, useUpgradeProject } from "@app/hooks/api";
|
|
||||||
import { Workspace } from "@app/hooks/api/types";
|
|
||||||
import { workspaceKeys } from "@app/hooks/api/workspace/queries";
|
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
|
||||||
import { queryClient } from "@app/reactQuery";
|
|
||||||
|
|
||||||
import { Button } from "../Button";
|
|
||||||
import { Tooltip } from "../Tooltip";
|
|
||||||
|
|
||||||
export type UpgradeProjectAlertProps = {
|
|
||||||
project: Workspace;
|
|
||||||
transparent?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UpgradeProjectAlert = ({
|
|
||||||
project,
|
|
||||||
transparent
|
|
||||||
}: UpgradeProjectAlertProps): JSX.Element | null => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { hasProjectRole } = useProjectPermission();
|
|
||||||
const upgradeProject = useUpgradeProject();
|
|
||||||
const [currentStatus, setCurrentStatus] = useState<string | null>(null);
|
|
||||||
const [isUpgrading, setIsUpgrading] = useState(false);
|
|
||||||
|
|
||||||
const isProjectAdmin = hasProjectRole("admin");
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: projectStatus,
|
|
||||||
isLoading: statusIsLoading,
|
|
||||||
refetch: manualProjectStatusRefetch
|
|
||||||
} = useGetUpgradeProjectStatus({
|
|
||||||
projectId: project.id,
|
|
||||||
enabled: isProjectAdmin && project.version === ProjectVersion.V1,
|
|
||||||
refetchInterval: 5_000,
|
|
||||||
onSuccess: (data) => {
|
|
||||||
if (!isProjectAdmin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data && data?.status !== null) {
|
|
||||||
if (data.status === "IN_PROGRESS") {
|
|
||||||
setCurrentStatus("Your upgrade is being processed.");
|
|
||||||
} else if (data.status === "FAILED") {
|
|
||||||
setCurrentStatus("Upgrade failed, please try again.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentStatus !== null && data?.status === null) {
|
|
||||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
|
||||||
router.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const onUpgradeProject = useCallback(async () => {
|
|
||||||
if (upgradeProject.isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsUpgrading(true);
|
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
|
||||||
|
|
||||||
if (!PRIVATE_KEY) {
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Private key not found"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await upgradeProject.mutateAsync({
|
|
||||||
projectId: project.id,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
manualProjectStatusRefetch();
|
|
||||||
|
|
||||||
setTimeout(() => setIsUpgrading(false), 5_000);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isLoading =
|
|
||||||
isUpgrading ||
|
|
||||||
((upgradeProject.isLoading ||
|
|
||||||
currentStatus !== null ||
|
|
||||||
(currentStatus === null && statusIsLoading)) &&
|
|
||||||
projectStatus?.status !== "FAILED");
|
|
||||||
|
|
||||||
if (project.version !== ProjectVersion.V1) return null;
|
|
||||||
|
|
||||||
if (transparent) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="solid"
|
|
||||||
size="md"
|
|
||||||
isLoading={isLoading}
|
|
||||||
isDisabled={isLoading || !isProjectAdmin}
|
|
||||||
onClick={onUpgradeProject}
|
|
||||||
>
|
|
||||||
Upgrade
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={twMerge(
|
|
||||||
"mt-4 flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white",
|
|
||||||
!isProjectAdmin && "opacity-80"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faWarning} className="pr-6 text-6xl text-white/80" />
|
|
||||||
<div className="flex w-full flex-col text-sm">
|
|
||||||
<span className="mb-2 text-lg font-semibold">Upgrade your project</span>
|
|
||||||
{isProjectAdmin ? (
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
Upgrade your project version to continue receiving the latest improvements and
|
|
||||||
patches.
|
|
||||||
</p>
|
|
||||||
<Link href="https://infisical.com/docs/documentation/platform/project-upgrade">
|
|
||||||
<a target="_blank" className="text-primary-400">
|
|
||||||
Learn more
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
<span className="font-bold">Please ask a project admin to upgrade the project.</span>
|
|
||||||
<br />
|
|
||||||
Upgrading the project version is required to continue receiving the latest
|
|
||||||
improvements and patches.
|
|
||||||
</p>
|
|
||||||
<Link href="https://infisical.com/docs/documentation/platform/project-upgrade">
|
|
||||||
<a target="_blank" className="text-primary-400">
|
|
||||||
Learn more
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{currentStatus && <p className="mt-2 opacity-80">Status: {currentStatus}</p>}
|
|
||||||
</div>
|
|
||||||
<div className="my-2">
|
|
||||||
<Tooltip
|
|
||||||
className={twMerge(isProjectAdmin && "hidden")}
|
|
||||||
content="You need to be an admin to upgrade the project."
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
isDisabled={isLoading || !isProjectAdmin}
|
|
||||||
onClick={onUpgradeProject}
|
|
||||||
>
|
|
||||||
Upgrade
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { UpgradeProjectAlert } from "./UpgradeProjectAlert";
|
|
||||||
@@ -21,6 +21,7 @@ export enum ProjectPermissionSub {
|
|||||||
IpAllowList = "ip-allowlist",
|
IpAllowList = "ip-allowlist",
|
||||||
Workspace = "workspace",
|
Workspace = "workspace",
|
||||||
Secrets = "secrets",
|
Secrets = "secrets",
|
||||||
|
SecretFolders = "secret-folders",
|
||||||
SecretRollback = "secret-rollback",
|
SecretRollback = "secret-rollback",
|
||||||
SecretApproval = "secret-approval",
|
SecretApproval = "secret-approval",
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
|
|||||||
@@ -194,7 +194,6 @@ const fetchSecretApprovalRequestDetails = async ({
|
|||||||
|
|
||||||
export const useGetSecretApprovalRequestDetails = ({
|
export const useGetSecretApprovalRequestDetails = ({
|
||||||
id,
|
id,
|
||||||
decryptKey,
|
|
||||||
options = {}
|
options = {}
|
||||||
}: TGetSecretApprovalRequestDetails & {
|
}: TGetSecretApprovalRequestDetails & {
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
@@ -210,7 +209,7 @@ export const useGetSecretApprovalRequestDetails = ({
|
|||||||
useQuery({
|
useQuery({
|
||||||
queryKey: secretApprovalRequestKeys.detail({ id }),
|
queryKey: secretApprovalRequestKeys.detail({ id }),
|
||||||
queryFn: () => fetchSecretApprovalRequestDetails({ id }),
|
queryFn: () => fetchSecretApprovalRequestDetails({ id }),
|
||||||
enabled: Boolean(id && decryptKey) && (options?.enabled ?? true)
|
enabled: Boolean(id) && (options?.enabled ?? true)
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => {
|
const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { UserWsKeyPair } from "../keys/types";
|
|
||||||
import { TSecretApprovalPolicy } from "../secretApproval/types";
|
import { TSecretApprovalPolicy } from "../secretApproval/types";
|
||||||
import { SecretV3Raw } from "../secrets/types";
|
import { SecretV3Raw } from "../secrets/types";
|
||||||
import { WsTag } from "../tags/types";
|
import { WsTag } from "../tags/types";
|
||||||
@@ -110,7 +109,6 @@ export type TGetSecretApprovalRequestCount = {
|
|||||||
|
|
||||||
export type TGetSecretApprovalRequestDetails = {
|
export type TGetSecretApprovalRequestDetails = {
|
||||||
id: string;
|
id: string;
|
||||||
decryptKey: UserWsKeyPair;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateSecretApprovalReviewStatusDTO = {
|
export type TUpdateSecretApprovalReviewStatusDTO = {
|
||||||
|
|||||||
@@ -286,12 +286,12 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
|
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
||||||
|
|
||||||
createNotification({ text: "Workspace created", type: "success" });
|
createNotification({ text: "Project created", type: "success" });
|
||||||
handlePopUpClose("addNewWs");
|
handlePopUpClose("addNewWs");
|
||||||
router.push(`/project/${newProjectId}/secrets/overview`);
|
router.push(`/project/${newProjectId}/secrets/overview`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
createNotification({ text: "Failed to create project", type: "error" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -565,11 +565,11 @@ const OrganizationPage = withPermission(
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
||||||
|
|
||||||
handlePopUpClose("addNewWs");
|
handlePopUpClose("addNewWs");
|
||||||
createNotification({ text: "Workspace created", type: "success" });
|
createNotification({ text: "Project created", type: "success" });
|
||||||
router.push(`/project/${newProjectId}/secrets/overview`);
|
router.push(`/project/${newProjectId}/secrets/overview`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
createNotification({ text: "Failed to create project", type: "error" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { TabSections, isTabSection } from "./TabSections";
|
import { isTabSection,TabSections } from "./TabSections";
|
||||||
|
|
||||||
export { TabSections, isTabSection };
|
export { isTabSection,TabSections };
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const UserAddToProjectModal = ({ membershipId, popUp, handlePopUpToggle }
|
|||||||
|
|
||||||
return (workspaces || []).filter(
|
return (workspaces || []).filter(
|
||||||
({ id, orgId: projectOrgId, version }) =>
|
({ id, orgId: projectOrgId, version }) =>
|
||||||
!wsWorkspaceIds.has(id) && projectOrgId === currentOrg?.id && version === ProjectVersion.V2
|
!wsWorkspaceIds.has(id) && projectOrgId === currentOrg?.id && version !== ProjectVersion.V1
|
||||||
);
|
);
|
||||||
}, [workspaces, projectMemberships]);
|
}, [workspaces, projectMemberships]);
|
||||||
|
|
||||||
|
|||||||
@@ -74,19 +74,12 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
decryptKey: wsKey,
|
decryptKey: wsKey,
|
||||||
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
|
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
|
||||||
});
|
});
|
||||||
} else if (currentWorkspace.version === ProjectVersion.V2) {
|
} else {
|
||||||
await addUserToWorkspaceNonE2EE({
|
await addUserToWorkspaceNonE2EE({
|
||||||
projectId: workspaceId,
|
projectId: workspaceId,
|
||||||
usernames: [orgUser.user.username],
|
usernames: [orgUser.user.username],
|
||||||
orgId
|
orgId
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to add user to project, unknown project type",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully added user to the project",
|
text: "Successfully added user to the project",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export const formSchema = z.object({
|
|||||||
permissions: z
|
permissions: z
|
||||||
.object({
|
.object({
|
||||||
secrets: z.record(multiEnvPermissionSchema).optional(),
|
secrets: z.record(multiEnvPermissionSchema).optional(),
|
||||||
|
"secret-folders": generalPermissionSchema.optional(),
|
||||||
member: generalPermissionSchema,
|
member: generalPermissionSchema,
|
||||||
groups: generalPermissionSchema,
|
groups: generalPermissionSchema,
|
||||||
identity: generalPermissionSchema,
|
identity: generalPermissionSchema,
|
||||||
@@ -158,7 +159,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
|||||||
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
||||||
if (rule === "secrets") {
|
if (rule === "secrets") {
|
||||||
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(actions || {})), rule);
|
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(actions || {})), rule);
|
||||||
} else {
|
} else if (actions) {
|
||||||
Object.entries(actions).forEach(([action, isAllowed]) => {
|
Object.entries(actions).forEach(([action, isAllowed]) => {
|
||||||
if (isAllowed) {
|
if (isAllowed) {
|
||||||
permissions.push({ subject: rule, action });
|
permissions.push({ subject: rule, action });
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Control, UseFormSetValue, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
import { Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
|
import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isEditable: boolean;
|
||||||
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
|
control: Control<TFormSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Permission {
|
||||||
|
SameAsSecrets = "same-as-secrets",
|
||||||
|
ReadOnly = "read-only"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RowPermissionSecretFoldersRow = ({ isEditable, setValue, control }: Props) => {
|
||||||
|
const formName = ProjectPermissionSub.SecretFolders;
|
||||||
|
const rule = useWatch({
|
||||||
|
control,
|
||||||
|
name: `permissions.${formName}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedPermissionCategory =
|
||||||
|
rule !== undefined ? Permission.ReadOnly : Permission.SameAsSecrets;
|
||||||
|
|
||||||
|
const handlePermissionChange = (val: Permission) => {
|
||||||
|
if (!val) return;
|
||||||
|
switch (val) {
|
||||||
|
case Permission.SameAsSecrets: {
|
||||||
|
setValue(`permissions.${formName}`, undefined, { shouldDirty: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Read-only
|
||||||
|
default:
|
||||||
|
setValue(
|
||||||
|
`permissions.${formName}`,
|
||||||
|
{
|
||||||
|
read: true,
|
||||||
|
edit: false,
|
||||||
|
create: false,
|
||||||
|
delete: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldDirty: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr>
|
||||||
|
<Td />
|
||||||
|
<Td>Secret Folders</Td>
|
||||||
|
<Td>
|
||||||
|
<Select
|
||||||
|
value={selectedPermissionCategory}
|
||||||
|
className="w-40 bg-mineshaft-600"
|
||||||
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
onValueChange={handlePermissionChange}
|
||||||
|
isDisabled={!isEditable}
|
||||||
|
>
|
||||||
|
<SelectItem value={Permission.SameAsSecrets}>Same as Secrets</SelectItem>
|
||||||
|
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
import { RolePermissionRow } from "./RolePermissionRow";
|
import { RolePermissionRow } from "./RolePermissionRow";
|
||||||
|
import { RowPermissionSecretFoldersRow } from "./RolePermissionSecretFoldersRow";
|
||||||
import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow";
|
import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow";
|
||||||
|
|
||||||
const SINGLE_PERMISSION_LIST = [
|
const SINGLE_PERMISSION_LIST = [
|
||||||
@@ -177,6 +178,11 @@ export const RolePermissionsSection = ({ roleSlug }: Props) => {
|
|||||||
getValue={getValues}
|
getValue={getValues}
|
||||||
control={control}
|
control={control}
|
||||||
/>
|
/>
|
||||||
|
<RowPermissionSecretFoldersRow
|
||||||
|
isEditable={isCustomRole}
|
||||||
|
setValue={setValue}
|
||||||
|
control={control}
|
||||||
|
/>
|
||||||
{SINGLE_PERMISSION_LIST.map((permission) => {
|
{SINGLE_PERMISSION_LIST.map((permission) => {
|
||||||
return (
|
return (
|
||||||
<RolePermissionRow
|
<RolePermissionRow
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { TabSections, isTabSection } from "./TabSections";
|
import { isTabSection,TabSections } from "./TabSections";
|
||||||
|
|
||||||
export { TabSections, isTabSection };
|
export { isTabSection,TabSections };
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { Button, ContentLoader, EmptyState, IconButton, Tooltip } from "@app/com
|
|||||||
import { useUser } from "@app/context";
|
import { useUser } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useGetSecretApprovalRequestDetails,
|
useGetSecretApprovalRequestDetails,
|
||||||
useGetUserWsKey,
|
|
||||||
useUpdateSecretApprovalReviewStatus
|
useUpdateSecretApprovalReviewStatus
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ApprovalStatus, CommitType } from "@app/hooks/api/types";
|
import { ApprovalStatus, CommitType } from "@app/hooks/api/types";
|
||||||
@@ -81,14 +80,12 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
workspaceId
|
workspaceId
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { user: userSession } = useUser();
|
const { user: userSession } = useUser();
|
||||||
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
|
|
||||||
const {
|
const {
|
||||||
data: secretApprovalRequestDetails,
|
data: secretApprovalRequestDetails,
|
||||||
isSuccess: isSecretApprovalRequestSuccess,
|
isSuccess: isSecretApprovalRequestSuccess,
|
||||||
isLoading: isSecretApprovalRequestLoading
|
isLoading: isSecretApprovalRequestLoading
|
||||||
} = useGetSecretApprovalRequestDetails({
|
} = useGetSecretApprovalRequestDetails({
|
||||||
id: approvalRequestId,
|
id: approvalRequestId
|
||||||
decryptKey: decryptFileKey!
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
UpgradePlanModal
|
UpgradePlanModal
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
|
useSubscription
|
||||||
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
|
import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
|
||||||
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
|
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
|
||||||
@@ -121,6 +126,12 @@ export const ActionBar = ({
|
|||||||
const { reset: resetSelectedSecret } = useSelectedSecretActions();
|
const { reset: resetSelectedSecret } = useSelectedSecretActions();
|
||||||
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
|
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
|
||||||
|
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
|
const shouldCheckFolderPermission = permission.rules.some((rule) =>
|
||||||
|
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
|
||||||
|
);
|
||||||
|
|
||||||
const debouncedOnSearch = debounce(onSearchChange, 500);
|
const debouncedOnSearch = debounce(onSearchChange, 500);
|
||||||
|
|
||||||
const handleFolderCreate = async (folderName: string) => {
|
const handleFolderCreate = async (folderName: string) => {
|
||||||
@@ -411,7 +422,12 @@ export const ActionBar = ({
|
|||||||
<div className="flex flex-col space-y-1 p-1.5">
|
<div className="flex flex-col space-y-1 p-1.5">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Create}
|
I={ProjectPermissionActions.Create}
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
a={subject(
|
||||||
|
shouldCheckFolderPermission
|
||||||
|
? ProjectPermissionSub.SecretFolders
|
||||||
|
: ProjectPermissionSub.Secrets,
|
||||||
|
{ environment, secretPath }
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ export const CreateSecretForm = ({
|
|||||||
title="Create secret"
|
title="Create secret"
|
||||||
subTitle="Add a secret to the particular environment and folder"
|
subTitle="Add a secret to the particular environment and folder"
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||||
<FormControl label="Key" isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
|
<FormControl label="Key" isRequired isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
|
||||||
<Input
|
<Input
|
||||||
{...register("key")}
|
{...register("key")}
|
||||||
placeholder="Type your secret name"
|
placeholder="Type your secret name"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { DeleteActionModal, IconButton, Modal, ModalContent } from "@app/components/v2";
|
import { DeleteActionModal, IconButton, Modal, ModalContent } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
|
import { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
|
||||||
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
|
||||||
@@ -36,6 +36,11 @@ export const FolderListView = ({
|
|||||||
"deleteFolder"
|
"deleteFolder"
|
||||||
] as const);
|
] as const);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
|
const shouldCheckFolderPermission = permission.rules.some((rule) =>
|
||||||
|
(rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)
|
||||||
|
);
|
||||||
|
|
||||||
const { mutateAsync: updateFolder } = useUpdateFolder();
|
const { mutateAsync: updateFolder } = useUpdateFolder();
|
||||||
const { mutateAsync: deleteFolder } = useDeleteFolder();
|
const { mutateAsync: deleteFolder } = useDeleteFolder();
|
||||||
@@ -128,7 +133,12 @@ export const FolderListView = ({
|
|||||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
a={subject(
|
||||||
|
shouldCheckFolderPermission
|
||||||
|
? ProjectPermissionSub.SecretFolders
|
||||||
|
: ProjectPermissionSub.Secrets,
|
||||||
|
{ environment, secretPath }
|
||||||
|
)}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Edit"
|
allowedLabel="Edit"
|
||||||
>
|
>
|
||||||
@@ -147,7 +157,12 @@ export const FolderListView = ({
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
a={subject(
|
||||||
|
shouldCheckFolderPermission
|
||||||
|
? ProjectPermissionSub.SecretFolders
|
||||||
|
: ProjectPermissionSub.Secrets,
|
||||||
|
{ environment, secretPath }
|
||||||
|
)}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Delete"
|
allowedLabel="Delete"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
|
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
@@ -64,7 +63,6 @@ import {
|
|||||||
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
|
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
|
||||||
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
|
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
|
||||||
import { SecretType, TSecretFolder } from "@app/hooks/api/types";
|
import { SecretType, TSecretFolder } from "@app/hooks/api/types";
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
|
||||||
|
|
||||||
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
|
import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
|
||||||
import { CreateSecretForm } from "./components/CreateSecretForm";
|
import { CreateSecretForm } from "./components/CreateSecretForm";
|
||||||
@@ -520,11 +518,6 @@ export const SecretOverviewPage = () => {
|
|||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{currentWorkspace?.version === ProjectVersion.V1 && (
|
|
||||||
<UpgradeProjectAlert project={currentWorkspace} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||||
<div className="flex flex-row items-center justify-center space-x-2">
|
<div className="flex flex-row items-center justify-center space-x-2">
|
||||||
|
|||||||
@@ -140,8 +140,8 @@ export const CreateSecretForm = ({
|
|||||||
title="Bulk Create & Update"
|
title="Bulk Create & Update"
|
||||||
subTitle="Create & update a secret across many environments"
|
subTitle="Create & update a secret across many environments"
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||||
<FormControl label="Key" isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
|
<FormControl label="Key" isRequired isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
|
||||||
<Input
|
<Input
|
||||||
{...register("key")}
|
{...register("key")}
|
||||||
placeholder="Type your secret name"
|
placeholder="Type your secret name"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback } from "react";
|
import { useCallback,useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -7,7 +7,7 @@ import { twMerge } from "tailwind-merge";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { IconButton, Tooltip, DeleteActionModal } from "@app/components/v2";
|
import { DeleteActionModal,IconButton, Tooltip } from "@app/components/v2";
|
||||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||||
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
|
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
|
||||||
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
|
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
|
||||||
@@ -9,6 +12,8 @@ import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/Rebu
|
|||||||
import { SecretTagsSection } from "../SecretTagsSection";
|
import { SecretTagsSection } from "../SecretTagsSection";
|
||||||
|
|
||||||
export const ProjectGeneralTab = () => {
|
export const ProjectGeneralTab = () => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ProjectNameChangeSection />
|
<ProjectNameChangeSection />
|
||||||
@@ -18,7 +23,7 @@ export const ProjectGeneralTab = () => {
|
|||||||
<PointInTimeVersionLimitSection />
|
<PointInTimeVersionLimitSection />
|
||||||
<AuditLogsRetentionSection />
|
<AuditLogsRetentionSection />
|
||||||
<BackfillSecretReferenceSecretion />
|
<BackfillSecretReferenceSecretion />
|
||||||
<RebuildSecretIndicesSection />
|
{currentWorkspace?.version !== ProjectVersion.V3 && <RebuildSecretIndicesSection />}
|
||||||
<DeleteProjectSection />
|
<DeleteProjectSection />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||