Compare commits

..

26 Commits

Author SHA1 Message Date
Sheen Capadngan
ba92192537 misc: removed creation limits completely 2024-08-06 19:41:09 +08:00
Sheen Capadngan
26ed8df73c misc: finalized list of license rate limits 2024-08-06 19:14:49 +08:00
Sheen Capadngan
1070954bdd misc: used destructuring 2024-08-06 02:05:13 +08:00
Sheen Capadngan
0f23b7e1d3 misc: added check for undefined orgId 2024-08-03 02:10:47 +08:00
Sheen Capadngan
33193a47ae misc: updated default onprem rate limits 2024-08-03 01:52:04 +08:00
Sheen Capadngan
1ad286ca87 misc: name updates and more comments 2024-08-02 22:58:53 +08:00
Sheen Capadngan
be7c11a3f5 Merge remote-tracking branch 'origin/main' into misc/add-org-level-rate-limit 2024-08-02 22:42:23 +08:00
Akhil Mohan
7f9150e60e Merge pull request #2226 from Infisical/maidul-wdqwdwf
Update docker-compose to docker compose in GHA
2024-08-02 19:54:17 +05:30
Maidul Islam
995f0360fb update docker-compsoe to docker compose 2024-08-02 10:22:21 -04:00
BlackMagiq
ecab69a7ab Merge pull request #2213 from Infisical/issue-cert-csr
Add Sign Certificate Endpoint for Certificate Issuance
2024-08-02 07:16:17 -07:00
Tuan Dang
cca36ab106 Merge remote-tracking branch 'origin' into issue-cert-csr 2024-08-02 07:06:58 -07:00
Tuan Dang
76311a1b5f Update DN parsing fn 2024-08-02 07:00:36 -07:00
Sheen Capadngan
55a6740714 misc: moved to plan-based rate limit 2024-08-02 21:37:48 +08:00
Sheen Capadngan
a0490d0fde Merge pull request #2220 from Infisical/feat/added-secret-folder-rbac
feat: added secret folder permissions
2024-08-02 19:05:12 +08:00
Maidul Islam
78e41a51c0 update workspace to project 2024-08-01 17:29:33 -04:00
Maidul Islam
8414f04e94 Merge pull request #2221 from akhilmhdh/feat/remove-migration-webhooks
feat: resolved invite failing and removed all unused things from frontend for previous upgrade
2024-08-01 11:18:50 -04:00
=
79e414ea9f feat: resolved invite failing and removed all unused things from frontend on previous upgrade 2024-08-01 20:12:23 +05:30
Maidul Islam
83772c1770 Merge pull request #2218 from GLEF1X/refactor/required-key-secret-input
refactor(secret-key-input): pass `isRequired` prop to secret key input
2024-08-01 10:35:23 -04:00
Sheen Capadngan
09928efba3 feat: added secret folder rbac' 2024-08-01 22:24:35 +08:00
Maidul Islam
48eb4e772f Merge pull request #2217 from akhilmhdh/feat/remove-migration-webhooks
feat: removed all the migration done for webhook and dynamic secret to KMS
2024-08-01 09:26:49 -04:00
GLEF1X
96cc315762 refactor(secret-key-input): pass isRequired prop to secret key input 2024-08-01 06:22:49 -04:00
=
e95d7e55c1 feat: removed all the migration done for webhook and dynamic secret towards kms encryption 2024-08-01 13:39:41 +05:30
Maidul Islam
520c068ac4 Merge pull request #2209 from Infisical/doc/add-documentation-for-kms-with-aws-hsm
doc: added documentation for using AWS HSM
2024-07-31 21:37:23 -04:00
Tuan Dang
9f0d7c6d11 Correct sign-certificate endpoint ref in docs 2024-07-31 14:04:52 -07:00
Tuan Dang
683e3dd7be Add sign certificate endpoint 2024-07-31 13:57:47 -07:00
Maidul Islam
46ca3856b3 change upgrade btn based on admin 2024-07-31 10:59:36 -04:00
68 changed files with 928 additions and 761 deletions

View File

@@ -22,14 +22,14 @@ jobs:
# uncomment this when testing locally using nektos/act
- uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }}
name: Install `docker-compose` for local simulations
name: Install `docker compose` for local simulations
with:
version: "2.14.2"
- name: 📦Build the latest image
run: docker build --tag infisical-api .
working-directory: backend
- name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
- name: Start the server
run: |
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
@@ -72,6 +72,6 @@ jobs:
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
- name: cleanup
run: |
docker-compose -f "docker-compose.dev.yml" down
docker compose -f "docker-compose.dev.yml" down
docker stop infisical-api
docker remove infisical-api

View File

@@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v3
- uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }}
name: Install `docker-compose` for local simulations
name: Install `docker compose` for local simulations
with:
version: "2.14.2"
- name: 🔧 Setup Node 20
@@ -33,7 +33,7 @@ jobs:
run: npm install
working-directory: backend
- name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
- name: Start integration test
run: npm run test:e2e
working-directory: backend
@@ -44,4 +44,4 @@ jobs:
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
- name: cleanup
run: |
docker-compose -f "docker-compose.dev.yml" down
docker compose -f "docker-compose.dev.yml" down

View File

@@ -18,6 +18,7 @@ import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-ser
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
import { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@@ -88,6 +89,7 @@ declare module "fastify" {
id: string;
orgId: string;
};
rateLimits: RateLimitConfiguration;
// passport data
passportUser: {
isUserCompleted: string;

View File

@@ -1,178 +1,8 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { Knex } from "knex";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { selectAllTableCols } from "@app/lib/knex/select";
import { SecretKeyEncoding, SecretType, TableName } from "../schemas";
import { SecretType, TableName } from "../schemas";
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> {
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");
});
}
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> {
@@ -356,49 +178,4 @@ export async function down(knex: Knex): Promise<void> {
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");
});
}
}

View File

@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasCreationLimitCol = await knex.schema.hasColumn(TableName.RateLimit, "creationLimit");
await knex.schema.alterTable(TableName.RateLimit, (t) => {
if (hasCreationLimitCol) {
t.dropColumn("creationLimit");
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasCreationLimitCol = await knex.schema.hasColumn(TableName.RateLimit, "creationLimit");
await knex.schema.alterTable(TableName.RateLimit, (t) => {
if (!hasCreationLimitCol) {
t.integer("creationLimit").defaultTo(30).notNullable();
}
});
}

View File

@@ -5,8 +5,6 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const DynamicSecretsSchema = z.object({
@@ -16,12 +14,16 @@ export const DynamicSecretsSchema = z.object({
type: z.string(),
defaultTTL: z.string(),
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(),
status: z.string().nullable().optional(),
statusDetails: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
encryptedConfig: zodBuffer
updatedAt: z.date()
});
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;

View File

@@ -15,7 +15,6 @@ export const RateLimitSchema = z.object({
authRateLimit: z.number().default(60),
inviteUserRateLimit: z.number().default(30),
mfaRateLimit: z.number().default(20),
creationLimit: z.number().default(30),
publicEndpointLimit: z.number().default(30),
createdAt: z.date(),
updatedAt: z.date()

View File

@@ -5,22 +5,27 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const WebhooksSchema = z.object({
id: z.string().uuid(),
secretPath: z.string().default("/"),
url: z.string(),
lastStatus: z.string().nullable().optional(),
lastRunErrorMessage: z.string().nullable().optional(),
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(),
updatedAt: z.date(),
envId: z.string().uuid(),
type: z.string().default("general").nullable().optional(),
encryptedSecretKeyWithKms: zodBuffer.nullable().optional(),
encryptedUrl: zodBuffer
urlCipherText: z.string().nullable().optional(),
urlIV: z.string().nullable().optional(),
urlTag: z.string().nullable().optional(),
type: z.string().default("general").nullable().optional()
});
export type TWebhooks = z.infer<typeof WebhooksSchema>;

View File

@@ -58,7 +58,6 @@ export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
authRateLimit: z.number(),
inviteUserRateLimit: z.number(),
mfaRateLimit: z.number(),
creationLimit: z.number(),
publicEndpointLimit: z.number()
}),
response: {

View File

@@ -136,6 +136,7 @@ export enum EventType {
IMPORT_CA_CERT = "import-certificate-authority-cert",
GET_CA_CRL = "get-certificate-authority-crl",
ISSUE_CERT = "issue-cert",
SIGN_CERT = "sign-cert",
GET_CERT = "get-cert",
DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert",
@@ -1143,6 +1144,15 @@ interface IssueCert {
};
}
interface SignCert {
type: EventType.SIGN_CERT;
metadata: {
caId: string;
dn: string;
serialNumber: string;
};
}
interface GetCert {
type: EventType.GET_CERT;
metadata: {
@@ -1333,6 +1343,7 @@ export type Event =
| ImportCaCert
| GetCaCrl
| IssueCert
| SignCert
| GetCert
| DeleteCert
| RevokeCert

View File

@@ -12,10 +12,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
.count("*")
.where({ dynamicSecretId })
.first();
const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first();
return parseInt(doc || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
@@ -24,7 +21,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
const doc = await (tx || db)(TableName.DynamicSecretLease)
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
.first()
.join(
@@ -40,10 +37,14 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"),
db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"),
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("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
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("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt")
);
@@ -58,12 +59,16 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
type: doc.dynType,
defaultTTL: doc.dynDefaultTTL,
maxTTL: doc.dynMaxTTL,
inputIV: doc.dynInputIV,
inputTag: doc.dynInputTag,
inputCiphertext: doc.dynInputCiphertext,
algorithm: doc.dynAlgorithm,
keyEncoding: doc.dynKeyEncoding,
folderId: doc.dynFolderId,
status: doc.dynStatus,
statusDetails: doc.dynStatusDetails,
createdAt: doc.dynCreatedAt,
updatedAt: doc.dynUpdatedAt,
encryptedConfig: doc.dynEncryptedConfig
updatedAt: doc.dynUpdatedAt
}
};
} catch (error) {

View File

@@ -1,9 +1,8 @@
import { SecretKeyEncoding } from "@app/db/schemas";
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 { 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 { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
@@ -15,8 +14,6 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
};
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
@@ -25,9 +22,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
queueService,
dynamicSecretDAL,
dynamicSecretProviders,
dynamicSecretLeaseDAL,
kmsService,
folderDAL
dynamicSecretLeaseDAL
}: TDynamicSecretLeaseQueueServiceFactoryDep) => {
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
await queueService.queue(
@@ -82,20 +77,15 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
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 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 dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
@@ -110,22 +100,17 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
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 });
if (dynamicSecretLeases.length) {
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const dynamicSecretInputConfig = secretManagerDecryptor({
cipherTextBlob: dynamicSecretCfg.encryptedConfig
}).toString();
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 Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
await Promise.all(

View File

@@ -1,14 +1,14 @@
import { ForbiddenError, subject } from "@casl/ability";
import ms from "ms";
import { SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
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 { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -34,7 +34,6 @@ type TDynamicSecretLeaseServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
@@ -47,8 +46,7 @@ export const dynamicSecretLeaseServiceFactory = ({
permissionService,
dynamicSecretQueueService,
projectDAL,
licenseService,
kmsService
licenseService
}: TDynamicSecretLeaseServiceFactoryDep) => {
const create = async ({
environmentSlug,
@@ -96,12 +94,14 @@ export const dynamicSecretLeaseServiceFactory = ({
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object;
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
const { maxTTL } = dynamicSecretCfg;
@@ -164,12 +164,14 @@ export const dynamicSecretLeaseServiceFactory = ({
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object;
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
const { maxTTL } = dynamicSecretCfg;
@@ -229,12 +231,14 @@ export const dynamicSecretLeaseServiceFactory = ({
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object;
const revokeResponse = await selectedProvider
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)

View File

@@ -1,11 +1,11 @@
import { ForbiddenError, subject } from "@casl/ability";
import { SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
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 { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -34,7 +34,6 @@ type TDynamicSecretServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
@@ -47,8 +46,7 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretProviders,
permissionService,
dynamicSecretQueueService,
projectDAL,
kmsService
projectDAL
}: TDynamicSecretServiceFactoryDep) => {
const create = async ({
path,
@@ -98,16 +96,16 @@ export const dynamicSecretServiceFactory = ({
const isConnected = await selectedProvider.validateConnection(provider.inputs);
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({
type: provider.type,
version: 1,
encryptedConfig,
inputIV: encryptedInput.iv,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
maxTTL,
defaultTTL,
folderId: folder.id,
@@ -167,28 +165,27 @@ export const dynamicSecretServiceFactory = ({
}
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const dynamicSecretInputConfig = secretManagerDecryptor({
cipherTextBlob: dynamicSecretCfg.encryptedConfig
}).toString();
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;
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
const isConnected = await selectedProvider.validateConnection(newInput);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const encryptedConfig = secretManagerEncryptor({
plainText: Buffer.from(JSON.stringify(updatedInput))
}).cipherTextBlob;
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput));
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
encryptedConfig,
inputIV: encryptedInput.iv,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
maxTTL,
defaultTTL,
name: newName ?? name,
@@ -289,16 +286,14 @@ export const dynamicSecretServiceFactory = ({
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const dynamicSecretInputConfig = secretManagerDecryptor({
cipherTextBlob: dynamicSecretCfg.encryptedConfig
}).toString();
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;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
return { ...dynamicSecretCfg, inputs: providerInputs };

View File

@@ -40,7 +40,12 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretRotation: true,
caCrl: false,
instanceUserManagement: false,
externalKms: false
externalKms: false,
rateLimits: {
readLimit: 60,
writeLimit: 200,
secretsLimit: 40
}
});
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

View File

@@ -58,6 +58,11 @@ export type TFeatureSet = {
caCrl: false;
instanceUserManagement: false;
externalKms: false;
rateLimits: {
readLimit: number;
writeLimit: number;
secretsLimit: number;
};
};
export type TOrgPlansTableDTO = {

View File

@@ -23,6 +23,7 @@ export enum ProjectPermissionSub {
IpAllowList = "ip-allowlist",
Project = "workspace",
Secrets = "secrets",
SecretFolders = "secret-folders",
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
SecretRotation = "secret-rotation",
@@ -42,6 +43,10 @@ export type ProjectPermissionSet =
ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
]
| [
ProjectPermissionActions,
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member]

View File

@@ -4,17 +4,16 @@ import { logger } from "@app/lib/logger";
import { TLicenseServiceFactory } from "../license/license-service";
import { TRateLimitDALFactory } from "./rate-limit-dal";
import { TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types";
import { RateLimitConfiguration, TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types";
let rateLimitMaxConfiguration = {
let rateLimitMaxConfiguration: RateLimitConfiguration = {
readLimit: 60,
publicEndpointLimit: 30,
writeLimit: 200,
secretsLimit: 60,
authRateLimit: 60,
inviteUserRateLimit: 30,
mfaRateLimit: 20,
creationLimit: 30
mfaRateLimit: 20
};
Object.freeze(rateLimitMaxConfiguration);
@@ -67,8 +66,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
secretsLimit: rateLimit.secretsRateLimit,
authRateLimit: rateLimit.authRateLimit,
inviteUserRateLimit: rateLimit.inviteUserRateLimit,
mfaRateLimit: rateLimit.mfaRateLimit,
creationLimit: rateLimit.creationLimit
mfaRateLimit: rateLimit.mfaRateLimit
};
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);

View File

@@ -5,7 +5,6 @@ export type TRateLimitUpdateDTO = {
authRateLimit: number;
inviteUserRateLimit: number;
mfaRateLimit: number;
creationLimit: number;
publicEndpointLimit: number;
};
@@ -14,3 +13,13 @@ export type TRateLimit = {
createdAt: Date;
updatedAt: Date;
} & TRateLimitUpdateDTO;
export type RateLimitConfiguration = {
readLimit: number;
publicEndpointLimit: number;
writeLimit: number;
secretsLimit: number;
authRateLimit: number;
inviteUserRateLimit: number;
mfaRateLimit: number;
};

View File

@@ -1056,7 +1056,7 @@ export const CERTIFICATE_AUTHORITIES = {
},
SIGN_INTERMEDIATE: {
caId: "The ID of the CA to sign the intermediate certificate with",
csr: "The CSR to sign with the CA",
csr: "The pem-encoded CSR to sign with the CA",
notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
maxPathLength:
@@ -1086,6 +1086,21 @@ export const CERTIFICATE_AUTHORITIES = {
privateKey: "The private key of the issued certificate",
serialNumber: "The serial number of the issued certificate"
},
SIGN_CERT: {
caId: "The ID of the CA to issue the certificate from",
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate",
altNames:
"A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.",
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
certificate: "The issued certificate",
issuingCaCertificate: "The certificate of the issuing CA",
certificateChain: "The certificate chain of the issued certificate",
serialNumber: "The serial number of the issued certificate"
},
GET_CRL: {
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
crl: "The certificate revocation list (CRL) of the CA"

View File

@@ -1,7 +1,6 @@
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
import { Redis } from "ioredis";
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
import { getConfig } from "@app/lib/config/env";
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
@@ -22,14 +21,16 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
// GET endpoints
export const readLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().readLimit,
hook: "preValidation",
max: (req) => req.rateLimits.readLimit,
keyGenerator: (req) => req.realIp
};
// POST, PATCH, PUT, DELETE endpoints
export const writeLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().writeLimit,
hook: "preValidation",
max: (req) => req.rateLimits.writeLimit,
keyGenerator: (req) => req.realIp
};
@@ -37,42 +38,40 @@ export const writeLimit: RateLimitOptions = {
export const secretsLimit: RateLimitOptions = {
// secrets, folders, secret imports
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().secretsLimit,
hook: "preValidation",
max: (req) => req.rateLimits.secretsLimit,
keyGenerator: (req) => req.realIp
};
export const authRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().authRateLimit,
hook: "preValidation",
max: (req) => req.rateLimits.authRateLimit,
keyGenerator: (req) => req.realIp
};
export const inviteUserRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().inviteUserRateLimit,
hook: "preValidation",
max: (req) => req.rateLimits.inviteUserRateLimit,
keyGenerator: (req) => req.realIp
};
export const mfaRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().mfaRateLimit,
hook: "preValidation",
max: (req) => req.rateLimits.mfaRateLimit,
keyGenerator: (req) => {
return req.headers.authorization?.split(" ")[1] || req.realIp;
}
};
export const creationLimit: RateLimitOptions = {
// identity, project, org
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().creationLimit,
keyGenerator: (req) => req.realIp
};
// Public endpoints to avoid brute force attacks
export const publicEndpointLimit: RateLimitOptions = {
// Read Shared Secrets
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().publicEndpointLimit,
hook: "preValidation",
max: (req) => req.rateLimits.publicEndpointLimit,
keyGenerator: (req) => req.realIp
};

View File

@@ -0,0 +1,38 @@
import fp from "fastify-plugin";
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
import { getConfig } from "@app/lib/config/env";
export const injectRateLimits = fp(async (server) => {
server.decorateRequest("rateLimits", null);
server.addHook("onRequest", async (req) => {
const appCfg = getConfig();
const instanceRateLimiterConfig = getRateLimiterConfig();
if (!req.auth?.orgId) {
// for public endpoints, we always use the instance-wide default rate limits
req.rateLimits = instanceRateLimiterConfig;
return;
}
const { rateLimits, customRateLimits } = await server.services.license.getPlan(req.auth.orgId);
if (customRateLimits && !appCfg.isCloud) {
// we do this because for self-hosted/dedicated instances, we want custom rate limits to be based on admin configuration
// note that the syncing of custom rate limit happens on the instanceRateLimiterConfig object
req.rateLimits = instanceRateLimiterConfig;
return;
}
// we're using the null coalescing operator in order to handle outdated licenses
req.rateLimits = {
readLimit: rateLimits?.readLimit ?? instanceRateLimiterConfig.readLimit,
writeLimit: rateLimits?.writeLimit ?? instanceRateLimiterConfig.writeLimit,
secretsLimit: rateLimits?.secretsLimit ?? instanceRateLimiterConfig.secretsLimit,
publicEndpointLimit: instanceRateLimiterConfig.publicEndpointLimit,
authRateLimit: instanceRateLimiterConfig.authRateLimit,
inviteUserRateLimit: instanceRateLimiterConfig.inviteUserRateLimit,
mfaRateLimit: instanceRateLimiterConfig.mfaRateLimit
};
});
});

View File

@@ -183,6 +183,7 @@ import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
import { injectAuditLogInfo } from "../plugins/audit-log";
import { injectIdentity } from "../plugins/auth/inject-identity";
import { injectPermission } from "../plugins/auth/inject-permission";
import { injectRateLimits } from "../plugins/inject-rate-limits";
import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2";
@@ -635,7 +636,8 @@ export const registerRoutes = async (
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL,
keyStore,
kmsService
kmsService,
projectBotDAL
});
const projectEnvService = projectEnvServiceFactory({
@@ -677,8 +679,7 @@ export const registerRoutes = async (
permissionService,
webhookDAL,
projectEnvDAL,
projectDAL,
kmsService
projectDAL
});
const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService });
@@ -988,9 +989,7 @@ export const registerRoutes = async (
queueService,
dynamicSecretLeaseDAL,
dynamicSecretProviders,
dynamicSecretDAL,
kmsService,
folderDAL
dynamicSecretDAL
});
const dynamicSecretService = dynamicSecretServiceFactory({
projectDAL,
@@ -1000,8 +999,7 @@ export const registerRoutes = async (
dynamicSecretProviders,
folderDAL,
permissionService,
licenseService,
kmsService
licenseService
});
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
projectDAL,
@@ -1011,8 +1009,7 @@ export const registerRoutes = async (
dynamicSecretLeaseDAL,
dynamicSecretProviders,
folderDAL,
licenseService,
kmsService
licenseService
});
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
auditLogDAL,
@@ -1134,6 +1131,7 @@ export const registerRoutes = async (
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
await server.register(injectPermission);
await server.register(injectRateLimits);
await server.register(injectAuditLogInfo);
server.route({

View File

@@ -129,7 +129,11 @@ export const SanitizedRoleSchema = ProjectRolesSchema.extend({
});
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
encryptedConfig: true
inputIV: true,
inputTag: true,
inputCiphertext: true,
keyEncoding: true,
algorithm: true
});
export const SanitizedAuditLogStreamSchema = z.object({

View File

@@ -337,7 +337,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId)
}),
body: z.object({
csr: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore),
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter),
maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength)
@@ -453,7 +453,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}),
body: z
.object({
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
ttl: z
@@ -516,4 +516,81 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "POST",
url: "/:caId/sign-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Sign certificate from CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId)
}),
body: z
.object({
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName),
commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
})
.refine(
(data) => {
const { ttl, notAfter } = data;
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
},
{
message: "Either ttl or notAfter must be present, but not both",
path: ["ttl", "notAfter"]
}
),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.certificate),
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
await server.services.certificateAuthority.signCertFromCa({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.SIGN_CERT,
metadata: {
caId: ca.id,
dn: ca.dn,
serialNumber
}
}
});
return {
certificate,
certificateChain,
issuingCaCertificate,
serialNumber
};
}
});
};

View File

@@ -3,7 +3,7 @@ import { z } from "zod";
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { IDENTITIES } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -16,7 +16,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/",
config: {
rateLimit: creationLimit
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {

View File

@@ -9,7 +9,7 @@ import {
UsersSchema
} from "@app/db/schemas";
import { ORGANIZATIONS } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@@ -307,7 +307,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/",
config: {
rateLimit: creationLimit
rateLimit: writeLimit
},
schema: {
body: z.object({

View File

@@ -4,7 +4,7 @@ import { z } from "zod";
import { CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECTS } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -142,7 +142,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
method: "POST",
url: "/",
config: {
rateLimit: creationLimit
rateLimit: writeLimit
},
schema: {
description: "Create a new project",

View File

@@ -18,6 +18,40 @@ export const createDistinguishedName = (parts: TDNParts) => {
return dnParts.join(", ");
};
export const parseDistinguishedName = (dn: string): TDNParts => {
const parts: TDNParts = {};
const dnParts = dn.split(/,\s*/);
for (const part of dnParts) {
const [key, value] = part.split("=");
switch (key.toUpperCase()) {
case "C":
parts.country = value;
break;
case "O":
parts.organization = value;
break;
case "OU":
parts.ou = value;
break;
case "ST":
parts.province = value;
break;
case "CN":
parts.commonName = value;
break;
case "L":
parts.locality = value;
break;
default:
// Ignore unrecognized keys
break;
}
}
return parts;
};
export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
switch (keyAlgorithm) {
case CertKeyAlgorithm.RSA_4096:

View File

@@ -22,7 +22,8 @@ import {
createDistinguishedName,
getCaCertChain,
getCaCredentials,
keyAlgorithmToAlgCfg
keyAlgorithmToAlgCfg,
parseDistinguishedName
} from "./certificate-authority-fns";
import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue";
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
@@ -36,6 +37,7 @@ import {
TGetCaDTO,
TImportCertToCaDTO,
TIssueCertFromCaDTO,
TSignCertFromCaDTO,
TSignIntermediateDTO,
TUpdateCaDTO
} from "./certificate-authority-types";
@@ -651,7 +653,8 @@ export const certificateAuthorityServiceFactory = ({
};
/**
* Return new leaf certificate issued by CA with id [caId]
* Return new leaf certificate issued by CA with id [caId] and private key.
* Note: private key and CSR are generated within Infisical.
*/
const issueCertFromCa = async ({
caId,
@@ -851,6 +854,204 @@ export const certificateAuthorityServiceFactory = ({
};
};
/**
* Return new leaf certificate issued by CA with id [caId].
* Note: CSR is generated externally and submitted to Infisical.
*/
const signCertFromCa = async ({
caId,
csr,
friendlyName,
commonName,
altNames,
ttl,
notBefore,
notAfter,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TSignCertFromCaDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1));
if (notAfter) {
notAfterDate = new Date(notAfter);
} else if (ttl) {
notAfterDate = new Date(new Date().getTime() + ms(ttl));
}
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const csrObj = new x509.Pkcs10CertificateRequest(csr);
const dn = parseDistinguishedName(csrObj.subject);
const cn = commonName || dn.commonName;
if (!cn)
throw new BadRequestError({
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
});
const { caPrivateKey } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const extensions: x509.Extension[] = [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
];
if (altNames) {
const altNamesArray: {
type: "email" | "dns";
value: string;
}[] = altNames
.split(",")
.map((name) => name.trim())
.map((altName) => {
// check if the altName is a valid email
if (z.string().email().safeParse(altName).success) {
return {
type: "email",
value: altName
};
}
// check if the altName is a valid hostname
if (hostnameRegex.test(altName)) {
return {
type: "dns",
value: altName
};
}
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
}
const serialNumber = crypto.randomBytes(32).toString("hex");
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
status: CertStatus.ACTIVE,
friendlyName: friendlyName || csrObj.subject,
commonName: cn,
altNames,
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate
},
tx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate
},
tx
);
return cert;
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
return {
certificate: leafCert.toString("pem"),
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
issuingCaCertificate,
serialNumber,
ca
};
};
return {
createCa,
getCaById,
@@ -860,6 +1061,7 @@ export const certificateAuthorityServiceFactory = ({
getCaCert,
signIntermediate,
importCertToCa,
issueCertFromCa
issueCertFromCa,
signCertFromCa
};
};

View File

@@ -81,6 +81,17 @@ export type TIssueCertFromCaDTO = {
notAfter?: string;
} & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO = {
caId: string;
csr: string;
friendlyName?: string;
commonName?: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
} & Omit<TProjectPermission, "projectId">;
export type TDNParts = {
commonName?: string;
organization?: string;

View File

@@ -540,7 +540,7 @@ export const projectMembershipServiceFactory = ({
const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found" });
if (project.version !== ProjectVersion.V2) {
if (project.version === ProjectVersion.V1) {
throw new BadRequestError({
message: "Please ask your project administrator to upgrade the project before leaving."
});

View File

@@ -22,6 +22,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
@@ -74,6 +75,7 @@ type TProjectServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
kmsService: Pick<
TKmsServiceFactory,
| "updateProjectSecretManagerKmsKey"
@@ -106,7 +108,8 @@ export const projectServiceFactory = ({
certificateAuthorityDAL,
certificateDAL,
keyStore,
kmsService
kmsService,
projectBotDAL
}: TProjectServiceFactoryDep) => {
/*
* Create workspace. Make user the admin
@@ -206,7 +209,26 @@ export const projectServiceFactory = ({
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
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);

View File

@@ -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));

View File

@@ -11,6 +11,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "./secret-folder-dal";
import { shouldCheckFolderPermission } from "./secret-folder-fns";
import {
TCreateFolderDTO,
TDeleteFolderDTO,
@@ -57,10 +58,21 @@ export const secretFolderServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
// we do this because we've split Secret and SecretFolder resources
// 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 });
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
@@ -148,10 +160,20 @@ export const secretFolderServiceFactory = ({
);
folders.forEach(({ environment, path: secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
// we do this because we've split Secret and SecretFolder resources
// 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.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) =>
@@ -243,10 +265,21 @@ export const secretFolderServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
// we do this because we've split Secret and SecretFolder resources
// 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);
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
@@ -316,10 +349,21 @@ export const secretFolderServiceFactory = ({
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
// we do this because we've split Secret and SecretFolder resources
// 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 });
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });

View File

@@ -1133,7 +1133,7 @@ export const secretQueueFactory = ({
});
queueService.start(QueueName.SecretWebhook, async (job) => {
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL, kmsService });
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL });
});
return {

View File

@@ -3,12 +3,12 @@ import crypto from "node:crypto";
import { AxiosError } from "axios";
import picomatch from "picomatch";
import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas";
import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
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 { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal";
@@ -16,12 +16,40 @@ import { WebhookType } from "./webhook-types";
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
export const triggerWebhookRequest = async (
{ webhookSecretKey: secretKey, webhookUrl: url }: { webhookSecretKey?: string; webhookUrl: string },
data: Record<string, unknown>
) => {
export const decryptWebhookDetails = (webhook: TWebhooks) => {
const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook;
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 payload = { ...data, timestamp: Date.now() };
const { secretKey, url } = decryptWebhookDetails(webhook);
if (secretKey) {
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">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
// this is reusable function
@@ -107,8 +134,7 @@ export const fnTriggerWebhook = async ({
projectId,
webhookDAL,
projectEnvDAL,
projectDAL,
kmsService
projectDAL
}: TFnTriggerWebhookDTO) => {
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
const toBeTriggeredHooks = webhooks.filter(
@@ -118,20 +144,10 @@ export const fnTriggerWebhook = async ({
if (!toBeTriggeredHooks.length) return;
logger.info("Secret webhook job started", { environment, secretPath, projectId });
const project = await projectDAL.findById(projectId);
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
projectId,
type: KmsDataKey.SecretManager
});
const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) => {
const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedUrl }).toString();
const webhookSecretKey = hook.encryptedSecretKeyWithKms
? kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedSecretKeyWithKms }).toString()
: undefined;
return triggerWebhookRequest(
{ webhookUrl, webhookSecretKey },
toBeTriggeredHooks.map((hook) =>
triggerWebhookRequest(
hook,
getWebhookPayload("secrets.modified", {
workspaceName: project.name,
workspaceId: projectId,
@@ -139,8 +155,8 @@ export const fnTriggerWebhook = async ({
secretPath,
type: hook.type
})
);
})
)
)
);
// filter hooks by status

View File

@@ -1,15 +1,15 @@
import { ForbiddenError } from "@casl/ability";
import { TWebhooksInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
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 { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal";
import { getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
import { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
import {
TCreateWebhookDTO,
TDeleteWebhookDTO,
@@ -23,7 +23,6 @@ type TWebhookServiceFactoryDep = {
projectEnvDAL: TProjectEnvDALFactory;
projectDAL: Pick<TProjectDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
@@ -32,8 +31,7 @@ export const webhookServiceFactory = ({
webhookDAL,
projectEnvDAL,
permissionService,
projectDAL,
kmsService
projectDAL
}: TWebhookServiceFactoryDep) => {
const createWebhook = async ({
actor,
@@ -58,28 +56,33 @@ export const webhookServiceFactory = ({
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) throw new BadRequestError({ message: "Env not found" });
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
projectId,
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,
const insertDoc: TWebhooksInsert = {
url: "", // deprecated - we are moving away from plaintext URLs
envId: env.id,
isDisabled: false,
secretPath: secretPath || "/",
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 };
};
@@ -133,18 +136,9 @@ export const webhookServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
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 {
await triggerWebhookRequest(
{ webhookUrl, webhookSecretKey },
webhook,
getWebhookPayload("test", {
workspaceName: project.name,
workspaceId: webhook.projectId,
@@ -183,15 +177,11 @@ export const webhookServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath);
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
return webhooks.map((w) => {
const decryptedUrl = kmsDataKeyDecryptor({ cipherTextBlob: w.encryptedUrl }).toString();
const { url } = decryptWebhookDetails(w);
return {
...w,
url: decryptedUrl
url
};
});
};

View File

@@ -0,0 +1,4 @@
---
title: "Sign certificate"
openapi: "POST /api/v1/pki/ca/{caId}/sign-certificate"
---

View File

@@ -74,7 +74,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
</Steps>
</Tab>
<Tab title="API">
To create a certificate, make an API request to the [Create Certificate](/api-reference/endpoints/certificate-authorities/sign-intermediate) API endpoint,
To create a certificate, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint,
specifying the issuing CA.
### Sample request
@@ -84,6 +84,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
--header 'Content-Type: application/json' \
--data-raw '{
"commonName": "My Certificate",
"ttl": "1y",
}'
```
@@ -103,6 +104,31 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
Make sure to store the `privateKey` as it is only returned once here at the time of certificate issuance. The `certificate` and `certificateChain` will remain accessible and can be retrieved at any time.
</Note>
If you have an external private key, you can also create a certificate by making an API request containing a pem-encoded CSR (Certificate Signing Request) to the [Sign Certificate](/api-reference/endpoints/certificates/sign-cert) API endpoint, specifying the issuing CA.
### Sample request
```bash Request
curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/sign-certificate' \
--header 'Content-Type: application/json' \
--data-raw '{
"csr": "...",
"ttl": "1y",
}'
```
### Sample response
```bash Response
{
certificate: "...",
certificateChain: "...",
issuingCaCertificate: "...",
privateKey: "...",
serialNumber: "..."
}
```
</Tab>
</Tabs>

View File

@@ -672,6 +672,7 @@
"api-reference/endpoints/certificate-authorities/sign-intermediate",
"api-reference/endpoints/certificate-authorities/import-cert",
"api-reference/endpoints/certificate-authorities/issue-cert",
"api-reference/endpoints/certificate-authorities/sign-cert",
"api-reference/endpoints/certificate-authorities/crl"
]
},

View File

@@ -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 />
);
};

View File

@@ -1 +0,0 @@
export { UpgradeOverlay } from "./UpgradeOverlay";

View File

@@ -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>
);
};

View File

@@ -1 +0,0 @@
export { UpgradeProjectAlert } from "./UpgradeProjectAlert";

View File

@@ -21,6 +21,7 @@ export enum ProjectPermissionSub {
IpAllowList = "ip-allowlist",
Workspace = "workspace",
Secrets = "secrets",
SecretFolders = "secret-folders",
SecretRollback = "secret-rollback",
SecretApproval = "secret-approval",
SecretRotation = "secret-rotation",

View File

@@ -5,6 +5,5 @@ export type TRateLimit = {
authRateLimit: number;
inviteUserRateLimit: number;
mfaRateLimit: number;
creationLimit: number;
publicEndpointLimit: number;
};

View File

@@ -194,7 +194,6 @@ const fetchSecretApprovalRequestDetails = async ({
export const useGetSecretApprovalRequestDetails = ({
id,
decryptKey,
options = {}
}: TGetSecretApprovalRequestDetails & {
options?: Omit<
@@ -210,7 +209,7 @@ export const useGetSecretApprovalRequestDetails = ({
useQuery({
queryKey: secretApprovalRequestKeys.detail({ id }),
queryFn: () => fetchSecretApprovalRequestDetails({ id }),
enabled: Boolean(id && decryptKey) && (options?.enabled ?? true)
enabled: Boolean(id) && (options?.enabled ?? true)
});
const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => {

View File

@@ -1,4 +1,3 @@
import { UserWsKeyPair } from "../keys/types";
import { TSecretApprovalPolicy } from "../secretApproval/types";
import { SecretV3Raw } from "../secrets/types";
import { WsTag } from "../tags/types";
@@ -110,7 +109,6 @@ export type TGetSecretApprovalRequestCount = {
export type TGetSecretApprovalRequestDetails = {
id: string;
decryptKey: UserWsKeyPair;
};
export type TUpdateSecretApprovalReviewStatusDTO = {

View File

@@ -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.
await new Promise((resolve) => setTimeout(resolve, 2_000));
createNotification({ text: "Workspace created", type: "success" });
createNotification({ text: "Project created", type: "success" });
handlePopUpClose("addNewWs");
router.push(`/project/${newProjectId}/secrets/overview`);
} catch (err) {
console.error(err);
createNotification({ text: "Failed to create workspace", type: "error" });
createNotification({ text: "Failed to create project", type: "error" });
}
};

View File

@@ -565,11 +565,11 @@ const OrganizationPage = withPermission(
await new Promise((resolve) => setTimeout(resolve, 2_000));
handlePopUpClose("addNewWs");
createNotification({ text: "Workspace created", type: "success" });
createNotification({ text: "Project created", type: "success" });
router.push(`/project/${newProjectId}/secrets/overview`);
} catch (err) {
console.error(err);
createNotification({ text: "Failed to create workspace", type: "error" });
createNotification({ text: "Failed to create project", type: "error" });
}
};

View File

@@ -1,3 +1,3 @@
import { TabSections, isTabSection } from "./TabSections";
import { isTabSection,TabSections } from "./TabSections";
export { TabSections, isTabSection };
export { isTabSection,TabSections };

View File

@@ -58,7 +58,7 @@ export const UserAddToProjectModal = ({ membershipId, popUp, handlePopUpToggle }
return (workspaces || []).filter(
({ id, orgId: projectOrgId, version }) =>
!wsWorkspaceIds.has(id) && projectOrgId === currentOrg?.id && version === ProjectVersion.V2
!wsWorkspaceIds.has(id) && projectOrgId === currentOrg?.id && version !== ProjectVersion.V1
);
}, [workspaces, projectMemberships]);

View File

@@ -74,19 +74,12 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
decryptKey: wsKey,
members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }]
});
} else if (currentWorkspace.version === ProjectVersion.V2) {
} else {
await addUserToWorkspaceNonE2EE({
projectId: workspaceId,
usernames: [orgUser.user.username],
orgId
});
} else {
createNotification({
text: "Failed to add user to project, unknown project type",
type: "error"
});
return;
}
createNotification({
text: "Successfully added user to the project",

View File

@@ -36,6 +36,7 @@ export const formSchema = z.object({
permissions: z
.object({
secrets: z.record(multiEnvPermissionSchema).optional(),
"secret-folders": generalPermissionSchema.optional(),
member: generalPermissionSchema,
groups: generalPermissionSchema,
identity: generalPermissionSchema,
@@ -158,7 +159,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
Object.entries(formVal || {}).forEach(([rule, actions]) => {
if (rule === "secrets") {
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(actions || {})), rule);
} else {
} else if (actions) {
Object.entries(actions).forEach(([action, isAllowed]) => {
if (isAllowed) {
permissions.push({ subject: rule, action });

View File

@@ -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>
);
};

View File

@@ -13,6 +13,7 @@ import {
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
import { RolePermissionRow } from "./RolePermissionRow";
import { RowPermissionSecretFoldersRow } from "./RolePermissionSecretFoldersRow";
import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow";
const SINGLE_PERMISSION_LIST = [
@@ -177,6 +178,11 @@ export const RolePermissionsSection = ({ roleSlug }: Props) => {
getValue={getValues}
control={control}
/>
<RowPermissionSecretFoldersRow
isEditable={isCustomRole}
setValue={setValue}
control={control}
/>
{SINGLE_PERMISSION_LIST.map((permission) => {
return (
<RolePermissionRow

View File

@@ -1,3 +1,3 @@
import { TabSections, isTabSection } from "./TabSections";
import { isTabSection,TabSections } from "./TabSections";
export { TabSections, isTabSection };
export { isTabSection,TabSections };

View File

@@ -16,7 +16,6 @@ import { Button, ContentLoader, EmptyState, IconButton, Tooltip } from "@app/com
import { useUser } from "@app/context";
import {
useGetSecretApprovalRequestDetails,
useGetUserWsKey,
useUpdateSecretApprovalReviewStatus
} from "@app/hooks/api";
import { ApprovalStatus, CommitType } from "@app/hooks/api/types";
@@ -81,14 +80,12 @@ export const SecretApprovalRequestChanges = ({
workspaceId
}: Props) => {
const { user: userSession } = useUser();
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const {
data: secretApprovalRequestDetails,
isSuccess: isSecretApprovalRequestSuccess,
isLoading: isSecretApprovalRequestLoading
} = useGetSecretApprovalRequestDetails({
id: approvalRequestId,
decryptKey: decryptFileKey!
id: approvalRequestId
});
const {

View File

@@ -44,7 +44,12 @@ import {
Tooltip,
UpgradePlanModal
} 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 { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api";
import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries";
@@ -121,6 +126,12 @@ export const ActionBar = ({
const { reset: resetSelectedSecret } = useSelectedSecretActions();
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 handleFolderCreate = async (folderName: string) => {
@@ -411,7 +422,12 @@ export const ActionBar = ({
<div className="flex flex-col space-y-1 p-1.5">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(
shouldCheckFolderPermission
? ProjectPermissionSub.SecretFolders
: ProjectPermissionSub.Secrets,
{ environment, secretPath }
)}
>
{(isAllowed) => (
<Button

View File

@@ -82,8 +82,8 @@ export const CreateSecretForm = ({
title="Create secret"
subTitle="Add a secret to the particular environment and folder"
>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<FormControl label="Key" isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
<FormControl label="Key" isRequired isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
<Input
{...register("key")}
placeholder="Type your secret name"

View File

@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
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 { useDeleteFolder, useUpdateFolder } from "@app/hooks/api";
import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
@@ -36,6 +36,11 @@ export const FolderListView = ({
"deleteFolder"
] as const);
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: 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">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(
shouldCheckFolderPermission
? ProjectPermissionSub.SecretFolders
: ProjectPermissionSub.Secrets,
{ environment, secretPath }
)}
renderTooltip
allowedLabel="Edit"
>
@@ -147,7 +157,12 @@ export const FolderListView = ({
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
a={subject(
shouldCheckFolderPermission
? ProjectPermissionSub.SecretFolders
: ProjectPermissionSub.Secrets,
{ environment, secretPath }
)}
renderTooltip
allowedLabel="Delete"
>

View File

@@ -42,7 +42,6 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
import {
ProjectPermissionActions,
ProjectPermissionSub,
@@ -64,7 +63,6 @@ import {
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/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 { CreateSecretForm } from "./components/CreateSecretForm";
@@ -520,11 +518,6 @@ export const SecretOverviewPage = () => {
.
</p>
</div>
{currentWorkspace?.version === ProjectVersion.V1 && (
<UpgradeProjectAlert project={currentWorkspace} />
)}
<div className="flex items-center justify-between">
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
<div className="flex flex-row items-center justify-center space-x-2">

View File

@@ -140,8 +140,8 @@ export const CreateSecretForm = ({
title="Bulk Create & Update"
subTitle="Create & update a secret across many environments"
>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<FormControl label="Key" isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
<FormControl label="Key" isRequired isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
<Input
{...register("key")}
placeholder="Type your secret name"

View File

@@ -1,4 +1,4 @@
import { useState, useCallback } from "react";
import { useCallback,useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
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 { 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 { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";

View File

@@ -92,7 +92,6 @@ export const SecretV2MigrationSection = () => {
<p className="mb-4 leading-7 text-gray-400">
Infisical secrets engine is now 10x faster and allows you to encrypt secrets with your own
KMS. Upgrade your project to receive these improvements.
<b>{!isAdmin && "This is an admin only operation."}</b>
</p>
<Button
onClick={() => handlePopUpOpen("migrationInfo")}
@@ -100,7 +99,7 @@ export const SecretV2MigrationSection = () => {
color="mineshaft"
isLoading={migrateProjectToV3.isLoading}
>
Upgrade Project
{ isAdmin ? "Upgrade Project" : "Upgrade requires admin privilege"}
</Button>
{didProjectUpgradeFailed && (
<p className="mt-2 text-sm leading-7 text-red-400">

View File

@@ -1,3 +1,6 @@
import { useWorkspace } from "@app/context";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
@@ -9,6 +12,8 @@ import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/Rebu
import { SecretTagsSection } from "../SecretTagsSection";
export const ProjectGeneralTab = () => {
const { currentWorkspace } = useWorkspace();
return (
<div>
<ProjectNameChangeSection />
@@ -18,7 +23,7 @@ export const ProjectGeneralTab = () => {
<PointInTimeVersionLimitSection />
<AuditLogsRetentionSection />
<BackfillSecretReferenceSecretion />
<RebuildSecretIndicesSection />
{currentWorkspace?.version !== ProjectVersion.V3 && <RebuildSecretIndicesSection />}
<DeleteProjectSection />
</div>
);

View File

@@ -15,7 +15,6 @@ const formSchema = z.object({
authRateLimit: z.number(),
inviteUserRateLimit: z.number(),
mfaRateLimit: z.number(),
creationLimit: z.number(),
publicEndpointLimit: z.number()
});
@@ -41,7 +40,6 @@ export const RateLimitPanel = () => {
authRateLimit: rateLimit?.authRateLimit ?? 60,
inviteUserRateLimit: rateLimit?.inviteUserRateLimit ?? 30,
mfaRateLimit: rateLimit?.mfaRateLimit ?? 20,
creationLimit: rateLimit?.creationLimit ?? 30,
publicEndpointLimit: rateLimit?.publicEndpointLimit ?? 30
}
});
@@ -60,7 +58,6 @@ export const RateLimitPanel = () => {
authRateLimit,
inviteUserRateLimit,
mfaRateLimit,
creationLimit,
publicEndpointLimit
} = formData;
@@ -71,7 +68,6 @@ export const RateLimitPanel = () => {
authRateLimit,
inviteUserRateLimit,
mfaRateLimit,
creationLimit,
publicEndpointLimit
});
createNotification({
@@ -210,25 +206,6 @@ export const RateLimitPanel = () => {
</FormControl>
)}
/>
<Controller
control={control}
defaultValue={300}
name="creationLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="New resource creation requests per minute"
className="w-72"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue={300}