mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
Compare commits
74 Commits
infisical-
...
maidu-2321
Author | SHA1 | Date | |
---|---|---|---|
a42f3b3763 | |||
f7d882a6fc | |||
385afdfcf8 | |||
281d703cc3 | |||
6f56ed5474 | |||
254446c895 | |||
2739b08e59 | |||
ba5e877a3b | |||
d2752216f6 | |||
d91fb0db02 | |||
4892eea009 | |||
09c6fcb73b | |||
79181a1e3d | |||
bb934ef7b1 | |||
cd9316537d | |||
942e5f2f65 | |||
353d231a4e | |||
68e05b7198 | |||
4f998e3940 | |||
1248840dc8 | |||
64c8125e4b | |||
8d33647739 | |||
d1c142e5b1 | |||
bb1cad0c5b | |||
2a1cfe15b4 | |||
881d70bc64 | |||
902a0b0ed4 | |||
ba92192537 | |||
26ed8df73c | |||
c1decab912 | |||
216c073290 | |||
1070954bdd | |||
cc689d3178 | |||
e6848828f2 | |||
c8b93e4467 | |||
0bca24bb00 | |||
c563ada50f | |||
26d1616e22 | |||
5fd071d1de | |||
9721d7a15e | |||
93db5c4555 | |||
ad4393fdef | |||
cd06e4e7f3 | |||
8e53a1b171 | |||
71af463ad8 | |||
7abd18b11c | |||
1aee50a751 | |||
0f23b7e1d3 | |||
e9b37a1f98 | |||
33193a47ae | |||
43fded2350 | |||
7b6f4d810d | |||
1ad286ca87 | |||
be7c11a3f5 | |||
55a6740714 | |||
7467a05fc4 | |||
afba636850 | |||
891cb06de0 | |||
02e8f20cbf | |||
d5f4ce4376 | |||
85653a90d5 | |||
879ef2c178 | |||
8777cfe680 | |||
2b630f75aa | |||
91cee20cc8 | |||
4249ec6030 | |||
e7a95e6af2 | |||
a9f04a3c1f | |||
3d380710ee | |||
2177ec6bcc | |||
070eb2aacd | |||
e619cfa313 | |||
c3038e3ca1 | |||
ff0e7feeee |
.github/workflows
backend
package-lock.jsonpackage.json
src
@types
db
ee
routes/v1
services
audit-log
license
rate-limit
secret-replication
server
config
plugins
routes
services
integration-auth
integration
project-bot
secret-v2-bridge
cli/packages
company
docs
frontend/src
components/v2/DeleteActionModal
helpers
hooks/api
views
IntegrationsPage
SecretMainPage/components/CreateSecretForm
SecretOverviewPage/components/CreateSecretForm
admin/DashboardPage
2
.github/workflows/run-cli-tests.yml
vendored
2
.github/workflows/run-cli-tests.yml
vendored
@ -50,6 +50,6 @@ jobs:
|
||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
|
||||
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
||||
INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||
# INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||
|
||||
run: go test -v -count=1 ./test
|
||||
|
55
backend/package-lock.json
generated
55
backend/package-lock.json
generated
@ -25,6 +25,7 @@
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
@ -7812,19 +7813,45 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-retry": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz",
|
||||
"integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-5.0.5.tgz",
|
||||
"integrity": "sha512-sB1RWMhSrre02Atv95K6bhESlJ/sPdZkK/wE/w1IdSCe0yM6FxSjksLa6T7aAvxvxlLKzQEC4KIiqpqyov1Tbg==",
|
||||
"dependencies": {
|
||||
"@octokit/request-error": "^5.0.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"@octokit/request-error": "^4.0.1",
|
||||
"@octokit/types": "^10.0.0",
|
||||
"bottleneck": "^2.15.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=5"
|
||||
"@octokit/core": ">=3"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": {
|
||||
"version": "18.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz",
|
||||
"integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw=="
|
||||
},
|
||||
"node_modules/@octokit/plugin-retry/node_modules/@octokit/request-error": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-4.0.2.tgz",
|
||||
"integrity": "sha512-uqwUEmZw3x4I9DGYq9fODVAAvcLsPQv97NRycP6syEFu5916M189VnNBW2zANNwqg3OiligNcAey7P0SET843w==",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^10.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"once": "^1.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-retry/node_modules/@octokit/types": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz",
|
||||
"integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-throttling": {
|
||||
@ -17396,6 +17423,22 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/probot/node_modules/@octokit/plugin-retry": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz",
|
||||
"integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==",
|
||||
"dependencies": {
|
||||
"@octokit/request-error": "^5.0.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"bottleneck": "^2.15.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/probot/node_modules/commander": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
||||
|
@ -121,6 +121,7 @@
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -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";
|
||||
@ -89,6 +90,7 @@ declare module "fastify" {
|
||||
id: string;
|
||||
orgId: string;
|
||||
};
|
||||
rateLimits: RateLimitConfiguration;
|
||||
// passport data
|
||||
passportUser: {
|
||||
isUserCompleted: string;
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
@ -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()
|
||||
|
@ -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: {
|
||||
|
@ -338,6 +338,7 @@ interface DeleteIntegrationEvent {
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
shouldDeleteIntegrationSecrets?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -58,6 +58,11 @@ export type TFeatureSet = {
|
||||
caCrl: false;
|
||||
instanceUserManagement: false;
|
||||
externalKms: false;
|
||||
rateLimits: {
|
||||
readLimit: number;
|
||||
writeLimit: number;
|
||||
secretsLimit: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -449,7 +449,7 @@ export const secretReplicationServiceFactory = ({
|
||||
});
|
||||
}
|
||||
if (locallyDeletedSecrets.length) {
|
||||
await secretDAL.delete(
|
||||
await secretV2BridgeDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
id: locallyDeletedSecrets.map(({ id }) => id)
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
|
38
backend/src/server/plugins/inject-rate-limits.ts
Normal file
38
backend/src/server/plugins/inject-rate-limits.ts
Normal 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
|
||||
};
|
||||
});
|
||||
});
|
@ -184,6 +184,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";
|
||||
@ -896,8 +897,15 @@ export const registerRoutes = async (
|
||||
folderDAL,
|
||||
integrationDAL,
|
||||
integrationAuthDAL,
|
||||
secretQueueService
|
||||
secretQueueService,
|
||||
integrationAuthService,
|
||||
projectBotService,
|
||||
secretV2BridgeDAL,
|
||||
secretImportDAL,
|
||||
secretDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const serviceTokenService = serviceTokenServiceFactory({
|
||||
projectEnvDAL,
|
||||
serviceTokenDAL,
|
||||
@ -1142,6 +1150,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({
|
||||
|
@ -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: {
|
||||
|
@ -170,6 +170,12 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
params: z.object({
|
||||
integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
shouldDeleteIntegrationSecrets: z
|
||||
.enum(["true", "false"])
|
||||
.optional()
|
||||
.transform((val) => val === "true")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
integration: IntegrationsSchema
|
||||
@ -183,7 +189,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.integrationId
|
||||
id: req.params.integrationId,
|
||||
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
@ -205,7 +212,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
region: integration.region,
|
||||
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
|
||||
// eslint-disable-next-line
|
||||
}) as any
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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",
|
||||
|
@ -0,0 +1,357 @@
|
||||
import { retry } from "@octokit/plugin-retry";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
|
||||
import { TIntegrationAuths, TIntegrations } from "@app/db/schemas";
|
||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
|
||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import { TIntegrationAuthServiceFactory } from "./integration-auth-service";
|
||||
import { Integrations } from "./integration-list";
|
||||
|
||||
const MAX_SYNC_SECRET_DEPTH = 5;
|
||||
|
||||
/**
|
||||
* Return the secrets in a given [folderId] including secrets from
|
||||
* nested imported folders recursively.
|
||||
*/
|
||||
const getIntegrationSecretsV2 = async (
|
||||
dto: {
|
||||
projectId: string;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
depth: number;
|
||||
decryptor: (value: Buffer | null | undefined) => string;
|
||||
},
|
||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">,
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
|
||||
) => {
|
||||
const content: Record<string, boolean> = {};
|
||||
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
|
||||
);
|
||||
return content;
|
||||
}
|
||||
|
||||
// process secrets in current folder
|
||||
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId);
|
||||
|
||||
secrets.forEach((secret) => {
|
||||
const secretKey = secret.key;
|
||||
content[secretKey] = true;
|
||||
});
|
||||
|
||||
// check if current folder has any imports from other folders
|
||||
const secretImports = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
|
||||
|
||||
// if no imports then return secrets in the current folder
|
||||
if (!secretImports.length) return content;
|
||||
const importedSecrets = await fnSecretsV2FromImports({
|
||||
decryptor: dto.decryptor,
|
||||
folderDAL,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretImportDAL,
|
||||
allowedImports: secretImports
|
||||
});
|
||||
|
||||
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
|
||||
const importedSecret = importedSecrets[i].secrets[j];
|
||||
if (!content[importedSecret.key]) {
|
||||
content[importedSecret.key] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the secrets in a given [folderId] including secrets from
|
||||
* nested imported folders recursively.
|
||||
*/
|
||||
const getIntegrationSecretsV1 = async (
|
||||
dto: {
|
||||
projectId: string;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
key: string;
|
||||
depth: number;
|
||||
},
|
||||
secretDAL: Pick<TSecretDALFactory, "findByFolderId">,
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
|
||||
) => {
|
||||
let content: Record<string, boolean> = {};
|
||||
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
|
||||
);
|
||||
return content;
|
||||
}
|
||||
|
||||
// process secrets in current folder
|
||||
const secrets = await secretDAL.findByFolderId(dto.folderId);
|
||||
secrets.forEach((secret) => {
|
||||
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key: dto.key
|
||||
});
|
||||
|
||||
content[secretKey] = true;
|
||||
});
|
||||
|
||||
// check if current folder has any imports from other folders
|
||||
const secretImport = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
|
||||
|
||||
// if no imports then return secrets in the current folder
|
||||
if (!secretImport) return content;
|
||||
|
||||
const importedFolders = await folderDAL.findByManySecretPath(
|
||||
secretImport.map(({ importEnv, importPath }) => ({
|
||||
envId: importEnv.id,
|
||||
secretPath: importPath
|
||||
}))
|
||||
);
|
||||
|
||||
for await (const folder of importedFolders) {
|
||||
if (folder) {
|
||||
// get secrets contained in each imported folder by recursively calling
|
||||
// this function against the imported folder
|
||||
const importedSecrets = await getIntegrationSecretsV1(
|
||||
{
|
||||
environment: dto.environment,
|
||||
projectId: dto.projectId,
|
||||
folderId: folder.id,
|
||||
key: dto.key,
|
||||
depth: dto.depth + 1
|
||||
},
|
||||
secretDAL,
|
||||
folderDAL,
|
||||
secretImportDAL
|
||||
);
|
||||
|
||||
// add the imported secrets to the current folder secrets
|
||||
content = { ...importedSecrets, ...content };
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export const deleteGithubSecrets = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: Omit<TIntegrations, "envId">;
|
||||
secrets: Record<string, boolean>;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
interface GitHubSecret {
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
visibility?: "all" | "private" | "selected";
|
||||
selected_repositories_url?: string | undefined;
|
||||
}
|
||||
|
||||
const OctokitWithRetry = Octokit.plugin(retry);
|
||||
const octokit = new OctokitWithRetry({
|
||||
auth: accessToken
|
||||
});
|
||||
|
||||
enum GithubScope {
|
||||
Repo = "github-repo",
|
||||
Org = "github-org",
|
||||
Env = "github-env"
|
||||
}
|
||||
|
||||
let encryptedGithubSecrets: GitHubSecret[];
|
||||
|
||||
switch (integration.scope) {
|
||||
case GithubScope.Org: {
|
||||
encryptedGithubSecrets = (
|
||||
await octokit.request("GET /orgs/{org}/actions/secrets", {
|
||||
org: integration.owner as string
|
||||
})
|
||||
).data.secrets;
|
||||
break;
|
||||
}
|
||||
case GithubScope.Env: {
|
||||
encryptedGithubSecrets = (
|
||||
await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
|
||||
repository_id: Number(integration.appId),
|
||||
environment_name: integration.targetEnvironmentId as string
|
||||
})
|
||||
).data.secrets;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
encryptedGithubSecrets = (
|
||||
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
|
||||
owner: integration.owner as string,
|
||||
repo: integration.app as string
|
||||
})
|
||||
).data.secrets;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for await (const encryptedSecret of encryptedGithubSecrets) {
|
||||
if (encryptedSecret.name in secrets) {
|
||||
switch (integration.scope) {
|
||||
case GithubScope.Org: {
|
||||
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
|
||||
org: integration.owner as string,
|
||||
secret_name: encryptedSecret.name
|
||||
});
|
||||
break;
|
||||
}
|
||||
case GithubScope.Env: {
|
||||
await octokit.request(
|
||||
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
|
||||
{
|
||||
repository_id: Number(integration.appId),
|
||||
environment_name: integration.targetEnvironmentId as string,
|
||||
secret_name: encryptedSecret.name
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
|
||||
owner: integration.owner as string,
|
||||
repo: integration.app as string,
|
||||
secret_name: encryptedSecret.name
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// small delay to prevent hitting API rate limits
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 50);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteIntegrationSecrets = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
integrationAuthService,
|
||||
projectBotService,
|
||||
secretV2BridgeDAL,
|
||||
folderDAL,
|
||||
secretDAL,
|
||||
secretImportDAL,
|
||||
kmsService
|
||||
}: {
|
||||
integration: Omit<TIntegrations, "envId"> & {
|
||||
projectId: string;
|
||||
environment: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
secretPath: string;
|
||||
};
|
||||
integrationAuth: TIntegrationAuths;
|
||||
integrationAuthService: Pick<TIntegrationAuthServiceFactory, "getIntegrationAccessToken" | "getIntegrationAuth">;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath" | "findBySecretPath">;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integration.projectId);
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: integration.projectId
|
||||
});
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(
|
||||
integration.projectId,
|
||||
integration.environment.slug,
|
||||
integration.secretPath
|
||||
);
|
||||
|
||||
if (!folder) {
|
||||
throw new NotFoundError({
|
||||
message: "Folder not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { accessToken } = await integrationAuthService.getIntegrationAccessToken(
|
||||
integrationAuth,
|
||||
shouldUseSecretV2Bridge,
|
||||
botKey
|
||||
);
|
||||
|
||||
const secrets = shouldUseSecretV2Bridge
|
||||
? await getIntegrationSecretsV2(
|
||||
{
|
||||
environment: integration.environment.id,
|
||||
projectId: integration.projectId,
|
||||
folderId: folder.id,
|
||||
depth: 1,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||
},
|
||||
secretV2BridgeDAL,
|
||||
folderDAL,
|
||||
secretImportDAL
|
||||
)
|
||||
: await getIntegrationSecretsV1(
|
||||
{
|
||||
environment: integration.environment.id,
|
||||
projectId: integration.projectId,
|
||||
folderId: folder.id,
|
||||
key: botKey as string,
|
||||
depth: 1
|
||||
},
|
||||
secretDAL,
|
||||
folderDAL,
|
||||
secretImportDAL
|
||||
);
|
||||
|
||||
const suffixedSecrets: typeof secrets = {};
|
||||
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
|
||||
|
||||
if (metadata) {
|
||||
Object.keys(secrets).forEach((key) => {
|
||||
const prefix = metadata?.secretPrefix || "";
|
||||
const suffix = metadata?.secretSuffix || "";
|
||||
const newKey = prefix + key + suffix;
|
||||
suffixedSecrets[newKey] = secrets[key];
|
||||
});
|
||||
}
|
||||
|
||||
switch (integration.integration) {
|
||||
case Integrations.GITHUB: {
|
||||
await deleteGithubSecrets({
|
||||
integration,
|
||||
accessToken,
|
||||
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new BadRequestError({
|
||||
message: "Invalid integration"
|
||||
});
|
||||
}
|
||||
};
|
@ -6,8 +6,15 @@ import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
||||
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
|
||||
import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||
import { TSecretQueueFactory } from "../secret/secret-queue";
|
||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import { TIntegrationDALFactory } from "./integration-dal";
|
||||
import {
|
||||
TCreateIntegrationDTO,
|
||||
@ -19,9 +26,15 @@ import {
|
||||
type TIntegrationServiceFactoryDep = {
|
||||
integrationDAL: TIntegrationDALFactory;
|
||||
integrationAuthDAL: TIntegrationAuthDALFactory;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||
integrationAuthService: TIntegrationAuthServiceFactory;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findByManySecretPath">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectBotService: TProjectBotServiceFactory;
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncIntegrations">;
|
||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
|
||||
};
|
||||
|
||||
export type TIntegrationServiceFactory = ReturnType<typeof integrationServiceFactory>;
|
||||
@ -31,7 +44,13 @@ export const integrationServiceFactory = ({
|
||||
integrationAuthDAL,
|
||||
folderDAL,
|
||||
permissionService,
|
||||
secretQueueService
|
||||
secretQueueService,
|
||||
integrationAuthService,
|
||||
projectBotService,
|
||||
secretV2BridgeDAL,
|
||||
secretImportDAL,
|
||||
kmsService,
|
||||
secretDAL
|
||||
}: TIntegrationServiceFactoryDep) => {
|
||||
const createIntegration = async ({
|
||||
app,
|
||||
@ -161,7 +180,14 @@ export const integrationServiceFactory = ({
|
||||
return updatedIntegration;
|
||||
};
|
||||
|
||||
const deleteIntegration = async ({ actorId, id, actor, actorAuthMethod, actorOrgId }: TDeleteIntegrationDTO) => {
|
||||
const deleteIntegration = async ({
|
||||
actorId,
|
||||
id,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
shouldDeleteIntegrationSecrets
|
||||
}: TDeleteIntegrationDTO) => {
|
||||
const integration = await integrationDAL.findById(id);
|
||||
if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
|
||||
|
||||
@ -174,6 +200,22 @@ export const integrationServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
|
||||
|
||||
const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId);
|
||||
|
||||
if (shouldDeleteIntegrationSecrets) {
|
||||
await deleteIntegrationSecrets({
|
||||
integration,
|
||||
integrationAuth,
|
||||
projectBotService,
|
||||
integrationAuthService,
|
||||
secretV2BridgeDAL,
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
secretDAL,
|
||||
kmsService
|
||||
});
|
||||
}
|
||||
|
||||
const deletedIntegration = await integrationDAL.transaction(async (tx) => {
|
||||
// delete integration
|
||||
const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);
|
||||
|
@ -63,6 +63,7 @@ export type TUpdateIntegrationDTO = {
|
||||
|
||||
export type TDeleteIntegrationDTO = {
|
||||
id: string;
|
||||
shouldDeleteIntegrationSecrets?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSyncIntegrationDTO = {
|
||||
|
@ -46,6 +46,7 @@ export const projectBotDALFactory = (db: TDbClient) => {
|
||||
const doc = await db
|
||||
.replicaNode()(TableName.ProjectMembership)
|
||||
.where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId)
|
||||
.where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId)
|
||||
.where(`${TableName.Users}.isGhost` as "isGhost", false)
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`)
|
||||
|
@ -66,10 +66,10 @@ export const getBotKeyFnFactory = (
|
||||
await projectBotDAL.create({
|
||||
name: "Infisical Bot (Ghost)",
|
||||
projectId,
|
||||
isActive: true,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: true,
|
||||
publicKey: botKey.publicKey,
|
||||
algorithm,
|
||||
keyEncoding: encoding,
|
||||
@ -80,6 +80,12 @@ export const getBotKeyFnFactory = (
|
||||
} else {
|
||||
await projectBotDAL.updateById(bot.id, {
|
||||
isActive: true,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
publicKey: botKey.publicKey,
|
||||
algorithm,
|
||||
keyEncoding: encoding,
|
||||
encryptedProjectKey: encryptedWorkspaceKey.ciphertext,
|
||||
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
|
||||
senderId: projectV1Keys.userId
|
||||
@ -89,7 +95,6 @@ export const getBotKeyFnFactory = (
|
||||
}
|
||||
|
||||
const botPrivateKey = getBotPrivateKey({ bot });
|
||||
|
||||
const botKey = decryptAsymmetric({
|
||||
ciphertext: bot.encryptedProjectKey,
|
||||
privateKey: botPrivateKey,
|
||||
|
@ -542,8 +542,8 @@ export const reshapeBridgeSecret = (
|
||||
secretPath,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
secretValue: secret.value,
|
||||
secretComment: secret.comment,
|
||||
secretValue: secret.value || "",
|
||||
secretComment: secret.comment || "",
|
||||
version: secret.version,
|
||||
type: secret.type,
|
||||
_id: secret.id,
|
||||
|
@ -490,10 +490,10 @@ export const secretV2BridgeServiceFactory = ({
|
||||
...secret,
|
||||
value: secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
comment: secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
})
|
||||
);
|
||||
const expandSecretReferences = expandSecretReferencesFactory({
|
||||
@ -522,7 +522,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
await expandSecretReferences(secretsGroupByKey);
|
||||
secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value;
|
||||
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value || "";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@ -13,13 +14,26 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AvailableVaultsAndDescriptions = []string{"auto (automatically select native vault on system)", "file (encrypted file vault)"}
|
||||
var AvailableVaults = []string{"auto", "file"}
|
||||
type VaultBackendType struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
var AvailableVaults = []VaultBackendType{
|
||||
{
|
||||
Name: "auto",
|
||||
Description: "automatically select the system keyring",
|
||||
},
|
||||
{
|
||||
Name: "file",
|
||||
Description: "encrypted file vault",
|
||||
},
|
||||
}
|
||||
|
||||
var vaultSetCmd = &cobra.Command{
|
||||
Example: `infisical vault set pass`,
|
||||
Use: "set [vault-name]",
|
||||
Short: "Used to set the vault backend to store your login details securely at rest",
|
||||
Example: `infisical vault set file`,
|
||||
Use: "set [file|auto]",
|
||||
Short: "Used to configure the vault backends",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
@ -35,15 +49,16 @@ var vaultSetCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
if wantedVaultTypeName == "auto" || wantedVaultTypeName == "file" {
|
||||
if wantedVaultTypeName == util.VAULT_BACKEND_AUTO_MODE || wantedVaultTypeName == util.VAULT_BACKEND_FILE_MODE {
|
||||
configFile, err := util.GetConfigFile()
|
||||
if err != nil {
|
||||
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
|
||||
return
|
||||
}
|
||||
|
||||
configFile.VaultBackendType = wantedVaultTypeName // save selected vault
|
||||
configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login
|
||||
configFile.VaultBackendType = wantedVaultTypeName
|
||||
configFile.LoggedInUserEmail = ""
|
||||
configFile.VaultBackendPassphrase = base64.StdEncoding.EncodeToString([]byte(util.GenerateRandomString(10)))
|
||||
|
||||
err = util.WriteConfigFile(&configFile)
|
||||
if err != nil {
|
||||
@ -55,7 +70,11 @@ var vaultSetCmd = &cobra.Command{
|
||||
|
||||
Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION))
|
||||
} else {
|
||||
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(AvailableVaults, ", "))
|
||||
var availableVaultsNames []string
|
||||
for _, vault := range AvailableVaults {
|
||||
availableVaultsNames = append(availableVaultsNames, vault.Name)
|
||||
}
|
||||
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(availableVaultsNames, ", "))
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -73,8 +92,8 @@ var vaultCmd = &cobra.Command{
|
||||
|
||||
func printAvailableVaultBackends() {
|
||||
fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:")
|
||||
for _, backend := range AvailableVaultsAndDescriptions {
|
||||
fmt.Printf("\n- %s", backend)
|
||||
for _, vaultType := range AvailableVaults {
|
||||
fmt.Printf("\n- %s (%s)", vaultType.Name, vaultType.Description)
|
||||
}
|
||||
|
||||
currentVaultBackend, err := util.GetCurrentVaultBackend()
|
||||
@ -89,5 +108,6 @@ func printAvailableVaultBackends() {
|
||||
|
||||
func init() {
|
||||
vaultCmd.AddCommand(vaultSetCmd)
|
||||
|
||||
rootCmd.AddCommand(vaultCmd)
|
||||
}
|
||||
|
@ -11,10 +11,11 @@ type UserCredentials struct {
|
||||
|
||||
// The file struct for Infisical config file
|
||||
type ConfigFile struct {
|
||||
LoggedInUserEmail string `json:"loggedInUserEmail"`
|
||||
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
|
||||
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
|
||||
VaultBackendType string `json:"vaultBackendType,omitempty"`
|
||||
LoggedInUserEmail string `json:"loggedInUserEmail"`
|
||||
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
|
||||
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
|
||||
VaultBackendType string `json:"vaultBackendType,omitempty"`
|
||||
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
|
||||
}
|
||||
|
||||
type LoggedInUser struct {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -50,10 +51,11 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
|
||||
}
|
||||
|
||||
configFile := models.ConfigFile{
|
||||
LoggedInUserEmail: userCredentials.Email,
|
||||
LoggedInUserDomain: config.INFISICAL_URL,
|
||||
LoggedInUsers: existingConfigFile.LoggedInUsers,
|
||||
VaultBackendType: existingConfigFile.VaultBackendType,
|
||||
LoggedInUserEmail: userCredentials.Email,
|
||||
LoggedInUserDomain: config.INFISICAL_URL,
|
||||
LoggedInUsers: existingConfigFile.LoggedInUsers,
|
||||
VaultBackendType: existingConfigFile.VaultBackendType,
|
||||
VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase,
|
||||
}
|
||||
|
||||
configFileMarshalled, err := json.Marshal(configFile)
|
||||
@ -215,6 +217,14 @@ func GetConfigFile() (models.ConfigFile, error) {
|
||||
return models.ConfigFile{}, err
|
||||
}
|
||||
|
||||
if configFile.VaultBackendPassphrase != "" {
|
||||
decodedPassphrase, err := base64.StdEncoding.DecodeString(configFile.VaultBackendPassphrase)
|
||||
if err != nil {
|
||||
return models.ConfigFile{}, fmt.Errorf("GetConfigFile: Unable to decode base64 passphrase [err=%s]", err)
|
||||
}
|
||||
os.Setenv("INFISICAL_VAULT_FILE_PASSPHRASE", string(decodedPassphrase))
|
||||
}
|
||||
|
||||
return configFile, nil
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,10 @@ const (
|
||||
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
|
||||
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
|
||||
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
|
||||
INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase.
|
||||
|
||||
VAULT_BACKEND_AUTO_MODE = "auto"
|
||||
VAULT_BACKEND_FILE_MODE = "file"
|
||||
|
||||
// Universal Auth
|
||||
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"
|
||||
@ -34,7 +38,8 @@ const (
|
||||
SERVICE_TOKEN_IDENTIFIER = "service-token"
|
||||
UNIVERSAL_AUTH_TOKEN_IDENTIFIER = "universal-auth-token"
|
||||
|
||||
INFISICAL_BACKUP_SECRET = "infisical-backup-secrets"
|
||||
INFISICAL_BACKUP_SECRET = "infisical-backup-secrets" // akhilmhdh: @depreciated remove in version v0.30
|
||||
INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY = "infisical-backup-secret-encryption-key"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@ -25,6 +26,8 @@ type DecodedSymmetricEncryptionDetails = struct {
|
||||
Key []byte
|
||||
}
|
||||
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {
|
||||
cipherx, err := base64.StdEncoding.DecodeString(cipher)
|
||||
if err != nil {
|
||||
@ -287,3 +290,11 @@ func GetCmdFlagOrEnv(cmd *cobra.Command, flag, envName string) (string, error) {
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func GenerateRandomString(length int) string {
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
@ -20,16 +24,39 @@ func SetValueInKeyring(key, value string) error {
|
||||
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
|
||||
}
|
||||
|
||||
return keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)
|
||||
err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)
|
||||
|
||||
if err != nil {
|
||||
log.Debug().Msg(fmt.Sprintf("Error while setting default keyring: %v", err))
|
||||
configFile, _ := GetConfigFile()
|
||||
|
||||
if configFile.VaultBackendPassphrase == "" {
|
||||
encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(GenerateRandomString(10))) // generate random passphrase
|
||||
configFile.VaultBackendPassphrase = encodedPassphrase
|
||||
configFile.VaultBackendType = VAULT_BACKEND_FILE_MODE
|
||||
err = WriteConfigFile(&configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We call this function at last to trigger the environment variable to be set
|
||||
GetConfigFile()
|
||||
}
|
||||
|
||||
err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value)
|
||||
log.Debug().Msg(fmt.Sprintf("Error while setting file keyring: %v", err))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func GetValueInKeyring(key string) (string, error) {
|
||||
currentVaultBackend, err := GetCurrentVaultBackend()
|
||||
if err != nil {
|
||||
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
|
||||
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical reset] then try again")
|
||||
}
|
||||
|
||||
return keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
|
||||
|
||||
}
|
||||
|
||||
func DeleteValueInKeyring(key string) error {
|
||||
|
@ -1,14 +1,15 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
@ -285,18 +286,25 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
|
||||
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", err)
|
||||
|
||||
if err == nil {
|
||||
WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, res.Secrets)
|
||||
backupEncryptionKey, err := GetBackupEncryptionKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey, res.Secrets)
|
||||
}
|
||||
|
||||
secretsToReturn = res.Secrets
|
||||
errorToReturn = err
|
||||
// only attempt to serve cached secrets if no internet connection and if at least one secret cached
|
||||
if !isConnected {
|
||||
backedSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath)
|
||||
if len(backedSecrets) > 0 {
|
||||
PrintWarning("Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug")
|
||||
secretsToReturn = backedSecrets
|
||||
errorToReturn = err
|
||||
backupEncryptionKey, _ := GetBackupEncryptionKey()
|
||||
if backupEncryptionKey != nil {
|
||||
backedUpSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey)
|
||||
if len(backedUpSecrets) > 0 {
|
||||
PrintWarning("Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug")
|
||||
secretsToReturn = backedUpSecrets
|
||||
errorToReturn = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -476,71 +484,99 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
|
||||
return secretsToReturn
|
||||
}
|
||||
|
||||
func WriteBackupSecrets(workspace string, environment string, secretsPath string, secrets []models.SingleEnvironmentVariable) error {
|
||||
var backedUpSecrets []models.BackupSecretKeyRing
|
||||
secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET)
|
||||
func GetBackupEncryptionKey() ([]byte, error) {
|
||||
encryptionKey, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY)
|
||||
if err != nil {
|
||||
if err == keyring.ErrUnsupportedPlatform {
|
||||
return errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
|
||||
} else if err != keyring.ErrNotFound {
|
||||
return fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
|
||||
return nil, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
|
||||
} else if err == keyring.ErrNotFound {
|
||||
// generate a new key
|
||||
randomizedKey := make([]byte, 16)
|
||||
rand.Read(randomizedKey)
|
||||
encryptionKey = hex.EncodeToString(randomizedKey)
|
||||
if err := SetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY, encryptionKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(encryptionKey), nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
|
||||
}
|
||||
}
|
||||
_ = json.Unmarshal([]byte(secretValueInKeyRing), &backedUpSecrets)
|
||||
return []byte(encryptionKey), nil
|
||||
}
|
||||
|
||||
backedUpSecrets = slices.DeleteFunc(backedUpSecrets, func(e models.BackupSecretKeyRing) bool {
|
||||
return e.SecretPath == secretsPath && e.ProjectID == workspace && e.Environment == environment
|
||||
})
|
||||
newBackupSecret := models.BackupSecretKeyRing{
|
||||
ProjectID: workspace,
|
||||
Environment: environment,
|
||||
SecretPath: secretsPath,
|
||||
Secrets: secrets,
|
||||
}
|
||||
backedUpSecrets = append(backedUpSecrets, newBackupSecret)
|
||||
func WriteBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error {
|
||||
formattedPath := strings.ReplaceAll(secretsPath, "/", "-")
|
||||
fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath)
|
||||
secrets_backup_folder_name := "secrets-backup"
|
||||
|
||||
listOfSecretsMarshalled, err := json.Marshal(backedUpSecrets)
|
||||
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("WriteBackupSecrets: unable to get full config folder path [err=%s]", err)
|
||||
}
|
||||
|
||||
err = SetValueInKeyring(INFISICAL_BACKUP_SECRET, string(listOfSecretsMarshalled))
|
||||
// create secrets backup directory
|
||||
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
|
||||
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
|
||||
err := os.Mkdir(fullPathToSecretsBackupFolder, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
marshaledSecrets, _ := json.Marshal(secrets)
|
||||
result, err := crypto.EncryptSymmetric(marshaledSecrets, encryptionKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("StoreUserCredsInKeyRing: unable to store user credentials because [err=%s]", err)
|
||||
return fmt.Errorf("WriteBackupSecrets: Unable to encrypt local secret backup to file [err=%s]", err)
|
||||
}
|
||||
listOfSecretsMarshalled, _ := json.Marshal(result)
|
||||
err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadBackupSecrets(workspace string, environment string, secretsPath string) ([]models.SingleEnvironmentVariable, error) {
|
||||
secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET)
|
||||
func ReadBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) {
|
||||
formattedPath := strings.ReplaceAll(secretsPath, "/", "-")
|
||||
fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath)
|
||||
secrets_backup_folder_name := "secrets-backup"
|
||||
|
||||
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
|
||||
if err != nil {
|
||||
if err == keyring.ErrUnsupportedPlatform {
|
||||
return nil, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
|
||||
} else if err == keyring.ErrNotFound {
|
||||
return nil, errors.New("credentials not found in system keyring")
|
||||
} else {
|
||||
return nil, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
|
||||
}
|
||||
return nil, fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err)
|
||||
}
|
||||
|
||||
var backedUpSecrets []models.BackupSecretKeyRing
|
||||
err = json.Unmarshal([]byte(secretValueInKeyRing), &backedUpSecrets)
|
||||
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
|
||||
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
encryptedBackupSecretsFilePath := fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName)
|
||||
|
||||
encryptedBackupSecretsAsBytes, err := os.ReadFile(encryptedBackupSecretsFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getUserCredsFromKeyRing: Something went wrong when unmarshalling user creds [err=%s]", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, backupSecret := range backedUpSecrets {
|
||||
if backupSecret.Environment == environment && backupSecret.ProjectID == workspace && backupSecret.SecretPath == secretsPath {
|
||||
return backupSecret.Secrets, nil
|
||||
}
|
||||
var encryptedBackUpSecrets models.SymmetricEncryptionResult
|
||||
err = json.Unmarshal(encryptedBackupSecretsAsBytes, &encryptedBackUpSecrets)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ReadBackupSecrets: unable to parse encrypted backup secrets. The secrets backup may be malformed [err=%s]", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
result, err := crypto.DecryptSymmetric(encryptionKey, encryptedBackUpSecrets.CipherText, encryptedBackUpSecrets.AuthTag, encryptedBackUpSecrets.Nonce)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ReadBackupSecrets: unable to decrypt encrypted backup secrets [err=%s]", err)
|
||||
}
|
||||
var plainTextSecrets []models.SingleEnvironmentVariable
|
||||
_ = json.Unmarshal(result, &plainTextSecrets)
|
||||
|
||||
return plainTextSecrets, nil
|
||||
|
||||
}
|
||||
|
||||
func DeleteBackupSecrets() error {
|
||||
// keeping this logic for now. Need to remove it later as more users migrate keyring would be used and this folder will be removed completely by then
|
||||
secrets_backup_folder_name := "secrets-backup"
|
||||
|
||||
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
|
||||
@ -549,8 +585,8 @@ func DeleteBackupSecrets() error {
|
||||
}
|
||||
|
||||
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
|
||||
|
||||
DeleteValueInKeyring(INFISICAL_BACKUP_SECRET)
|
||||
DeleteValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY)
|
||||
|
||||
return os.RemoveAll(fullPathToSecretsBackupFolder)
|
||||
}
|
||||
|
@ -11,11 +11,11 @@ func GetCurrentVaultBackend() (string, error) {
|
||||
}
|
||||
|
||||
if configFile.VaultBackendType == "" {
|
||||
return "auto", nil
|
||||
return VAULT_BACKEND_AUTO_MODE, nil
|
||||
}
|
||||
|
||||
if configFile.VaultBackendType != "auto" && configFile.VaultBackendType != "file" {
|
||||
return "auto", nil
|
||||
if configFile.VaultBackendType != VAULT_BACKEND_AUTO_MODE && configFile.VaultBackendType != VAULT_BACKEND_FILE_MODE {
|
||||
return VAULT_BACKEND_AUTO_MODE, nil
|
||||
}
|
||||
|
||||
return configFile.VaultBackendType, nil
|
||||
|
15
company/handbook/meetings.mdx
Normal file
15
company/handbook/meetings.mdx
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
title: "Meetings"
|
||||
sidebarTitle: "Meetings"
|
||||
description: "The guide to meetings at Infisical."
|
||||
---
|
||||
|
||||
## "Let's schedule a meeting about this"
|
||||
|
||||
Being a remote-first company, we try to be as async as possible. When an issue arises, it's best to create a public Slack thread and tag all the necessary team members. Otherwise, if you were to "put a meeting on a calendar", the decision making process will inevitable slow down by at least a day (e.g., trying to find the right time for folks in different time zones is not always straightforward).
|
||||
|
||||
In other words, we have almost no (recurring) meetings and prefer written communication or quick Slack huddles.
|
||||
|
||||
## Weekly All-hands
|
||||
|
||||
All-hands is the single recurring meeting that we run every Monday at 8:30am PT. Typically, we would discuss everything important that happened during the previous week and plan out the week ahead. This is also an opportunity to bring up any important topics in front of the whole company (but feel free to post those in Slack too).
|
@ -59,7 +59,8 @@
|
||||
"handbook/onboarding",
|
||||
"handbook/spending-money",
|
||||
"handbook/time-off",
|
||||
"handbook/hiring"
|
||||
"handbook/hiring",
|
||||
"handbook/meetings"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
@ -30,8 +30,5 @@ description: "Change the vault type in Infisical"
|
||||
|
||||
## Description
|
||||
|
||||
To safeguard your login details when using the CLI, Infisical places them in a system vault or an encrypted text file, protected by a passphrase that only the user knows.
|
||||
|
||||
<Tip>To avoid constantly entering your passphrase when using the `file` vault type, set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable with your password in your shell</Tip>
|
||||
|
||||
To safeguard your login details when using the CLI, Infisical attempts to store them in a system keyring. If a system keyring cannot be found on your machine, the data is stored in a config file.
|
||||
|
||||
|
@ -16,7 +16,7 @@ Before you begin, you'll first need to choose a method of authentication with AW
|
||||
<Steps>
|
||||
<Step title="Create the Managing User IAM Role">
|
||||
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
||||

|
||||

|
||||
|
||||
2. Select **AWS Account** as the **Trusted Entity Type**.
|
||||
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted.
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Kubernetes"
|
||||
title: "Kubernetes Operator"
|
||||
description: "How to use Infisical to inject secrets into Kubernetes clusters."
|
||||
---
|
||||
|
||||
@ -9,6 +9,10 @@ The Infisical Secrets Operator is a Kubernetes controller that retrieves secrets
|
||||
It uses an `InfisicalSecret` resource to specify authentication and storage methods.
|
||||
The operator continuously updates secrets and can also reload dependent deployments automatically.
|
||||
|
||||
<Note>
|
||||
If you are already using the External Secrets operator, you can you can view the integration documentation for it [here](https://external-secrets.io/latest/provider/infisical/).
|
||||
</Note>
|
||||
|
||||
## Install Operator
|
||||
|
||||
The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)
|
||||
|
@ -155,7 +155,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Key Management",
|
||||
"group": "Key Management (KMS)",
|
||||
"pages": [
|
||||
"documentation/platform/kms/overview",
|
||||
"documentation/platform/kms/aws-kms",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
@ -16,6 +16,7 @@ type Props = {
|
||||
subTitle?: string;
|
||||
onDeleteApproved: () => Promise<void>;
|
||||
buttonText?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const DeleteActionModal = ({
|
||||
@ -26,7 +27,8 @@ export const DeleteActionModal = ({
|
||||
onDeleteApproved,
|
||||
title,
|
||||
subTitle = "This action is irreversible.",
|
||||
buttonText = "Delete"
|
||||
buttonText = "Delete",
|
||||
children
|
||||
}: Props): JSX.Element => {
|
||||
const [inputData, setInputData] = useState("");
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
@ -94,9 +96,10 @@ export const DeleteActionModal = ({
|
||||
<Input
|
||||
value={inputData}
|
||||
onChange={(e) => setInputData(e.target.value)}
|
||||
placeholder="Type confirm..."
|
||||
placeholder={`Type ${deleteKey} here`}
|
||||
/>
|
||||
</FormControl>
|
||||
{children}
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
14
frontend/src/helpers/parseEnvVar.ts
Normal file
14
frontend/src/helpers/parseEnvVar.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/** Extracts the key and value from a passed in env string based on the provided delimiters. */
|
||||
export const getKeyValue = (pastedContent: string, delimiters: string[]) => {
|
||||
const foundDelimiter = delimiters.find((delimiter) => pastedContent.includes(delimiter));
|
||||
|
||||
if (!foundDelimiter) {
|
||||
return { key: pastedContent.trim(), value: "" };
|
||||
}
|
||||
|
||||
const [key, value] = pastedContent.split(foundDelimiter);
|
||||
return {
|
||||
key: key.trim(),
|
||||
value: (value ?? "").trim()
|
||||
};
|
||||
};
|
@ -110,8 +110,15 @@ export const useCreateIntegration = () => {
|
||||
export const useDeleteIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, { id: string; workspaceId: string }>({
|
||||
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`),
|
||||
return useMutation<
|
||||
{},
|
||||
{},
|
||||
{ id: string; workspaceId: string; shouldDeleteIntegrationSecrets: boolean }
|
||||
>({
|
||||
mutationFn: ({ id, shouldDeleteIntegrationSecrets }) =>
|
||||
apiRequest.delete(
|
||||
`/api/v1/integration/${id}?shouldDeleteIntegrationSecrets=${shouldDeleteIntegrationSecrets}`
|
||||
),
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId));
|
||||
|
@ -5,6 +5,5 @@ export type TRateLimit = {
|
||||
authRateLimit: number;
|
||||
inviteUserRateLimit: number;
|
||||
mfaRateLimit: number;
|
||||
creationLimit: number;
|
||||
publicEndpointLimit: number;
|
||||
};
|
||||
|
@ -106,9 +106,13 @@ export const IntegrationsPage = withProjectPermission(
|
||||
handleProviderIntegration(provider);
|
||||
};
|
||||
|
||||
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
|
||||
const handleIntegrationDelete = async (
|
||||
integrationId: string,
|
||||
shouldDeleteIntegrationSecrets: boolean,
|
||||
cb: () => void
|
||||
) => {
|
||||
try {
|
||||
await deleteIntegration({ id: integrationId, workspaceId });
|
||||
await deleteIntegration({ id: integrationId, workspaceId, shouldDeleteIntegrationSecrets });
|
||||
if (cb) cb();
|
||||
createNotification({
|
||||
type: "success",
|
||||
@ -152,7 +156,7 @@ export const IntegrationsPage = withProjectPermission(
|
||||
isLoading={isIntegrationLoading}
|
||||
integrations={integrations}
|
||||
environments={environments}
|
||||
onIntegrationDelete={({ id }, cb) => handleIntegrationDelete(id, cb)}
|
||||
onIntegrationDelete={handleIntegrationDelete}
|
||||
workspaceId={workspaceId}
|
||||
/>
|
||||
<CloudIntegrationSection
|
||||
|
@ -7,6 +7,7 @@ import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
FormLabel,
|
||||
@ -16,7 +17,7 @@ import {
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
|
||||
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
||||
import { TIntegration } from "@app/hooks/api/types";
|
||||
@ -25,7 +26,11 @@ type Props = {
|
||||
environments: Array<{ name: string; slug: string; id: string }>;
|
||||
integrations?: TIntegration[];
|
||||
isLoading?: boolean;
|
||||
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
|
||||
onIntegrationDelete: (
|
||||
integrationId: string,
|
||||
shouldDeleteIntegrationSecrets: boolean,
|
||||
cb: () => void
|
||||
) => Promise<void>;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
@ -37,10 +42,12 @@ export const IntegrationsSection = ({
|
||||
workspaceId
|
||||
}: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"deleteConfirmation"
|
||||
"deleteConfirmation",
|
||||
"deleteSecretsConfirmation"
|
||||
] as const);
|
||||
|
||||
const { mutate: syncIntegration } = useSyncIntegration();
|
||||
const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false);
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
@ -249,7 +256,10 @@ export const IntegrationsSection = ({
|
||||
<div className="flex items-end opacity-80 duration-200 hover:opacity-100">
|
||||
<Tooltip content="Remove Integration">
|
||||
<IconButton
|
||||
onClick={() => handlePopUpOpen("deleteConfirmation", integration)}
|
||||
onClick={() => {
|
||||
setShouldDeleteSecrets.off();
|
||||
handlePopUpOpen("deleteConfirmation", integration);
|
||||
}}
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="danger"
|
||||
@ -281,11 +291,49 @@ export const IntegrationsSection = ({
|
||||
(popUp?.deleteConfirmation?.data as TIntegration)?.integration ||
|
||||
""
|
||||
}
|
||||
onDeleteApproved={async () =>
|
||||
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () =>
|
||||
handlePopUpClose("deleteConfirmation")
|
||||
)
|
||||
}
|
||||
onDeleteApproved={async () => {
|
||||
if (shouldDeleteSecrets) {
|
||||
handlePopUpOpen("deleteSecretsConfirmation");
|
||||
return;
|
||||
}
|
||||
|
||||
await onIntegrationDelete(
|
||||
(popUp?.deleteConfirmation.data as TIntegration).id,
|
||||
false,
|
||||
() => handlePopUpClose("deleteConfirmation")
|
||||
);
|
||||
}}
|
||||
>
|
||||
{(popUp?.deleteConfirmation?.data as TIntegration)?.integration === "github" && (
|
||||
<div className="mt-4">
|
||||
<Checkbox
|
||||
id="delete-integration-secrets"
|
||||
checkIndicatorBg="text-white"
|
||||
onCheckedChange={() => setShouldDeleteSecrets.toggle()}
|
||||
>
|
||||
Delete previously synced secrets from the destination
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
</DeleteActionModal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSecretsConfirmation.isOpen}
|
||||
title={`Are you sure you also want to delete secrets on ${
|
||||
(popUp?.deleteConfirmation.data as TIntegration)?.integration
|
||||
}?`}
|
||||
subTitle="By confirming, you acknowledge that all secrets managed by this integration will be removed from the destination. This action is irreversible."
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretsConfirmation", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={async () => {
|
||||
await onIntegrationDelete(
|
||||
(popUp?.deleteConfirmation.data as TIntegration).id,
|
||||
true,
|
||||
() => {
|
||||
handlePopUpClose("deleteSecretsConfirmation");
|
||||
handlePopUpClose("deleteConfirmation");
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ClipboardEvent } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
@ -5,6 +6,7 @@ import { z } from "zod";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||
import { useCreateSecretV3 } from "@app/hooks/api";
|
||||
import { SecretType } from "@app/hooks/api/types";
|
||||
|
||||
@ -38,6 +40,7 @@ export const CreateSecretForm = ({
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { errors, isSubmitting }
|
||||
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
|
||||
const { isOpen } = usePopUpState(PopUpNames.CreateSecretForm);
|
||||
@ -73,6 +76,16 @@ export const CreateSecretForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
const delimitters = [":", "="];
|
||||
const pastedContent = e.clipboardData.getData("text");
|
||||
const { key, value } = getKeyValue(pastedContent, delimitters);
|
||||
|
||||
setValue("key", key);
|
||||
setValue("value", value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@ -83,10 +96,16 @@ export const CreateSecretForm = ({
|
||||
subTitle="Add a secret to the particular environment and folder"
|
||||
>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||
<FormControl label="Key" isRequired isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
|
||||
<FormControl
|
||||
label="Key"
|
||||
isRequired
|
||||
isError={Boolean(errors?.key)}
|
||||
errorText={errors?.key?.message}
|
||||
>
|
||||
<Input
|
||||
{...register("key")}
|
||||
placeholder="Type your secret name"
|
||||
onPaste={handlePaste}
|
||||
autoCapitalization={autoCapitalize}
|
||||
/>
|
||||
</FormControl>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ClipboardEvent } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@ -17,8 +18,9 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { getKeyValue } from "@app/helpers/parseEnvVar";
|
||||
import { useCreateFolder, useCreateSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
|
||||
import { SecretType,SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||
|
||||
const typeSchema = z
|
||||
.object({
|
||||
@ -54,6 +56,7 @@ export const CreateSecretForm = ({
|
||||
control,
|
||||
reset,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { isSubmitting, errors }
|
||||
} = useForm<TFormSchema>({ resolver: zodResolver(typeSchema) });
|
||||
const newSecretKey = watch("key");
|
||||
@ -133,6 +136,17 @@ export const CreateSecretForm = ({
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
const delimitters = [":", "="];
|
||||
const pastedContent = e.clipboardData.getData("text");
|
||||
const { key, value } = getKeyValue(pastedContent, delimitters);
|
||||
|
||||
setValue("key", key);
|
||||
setValue("value", value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onTogglePopUp}>
|
||||
<ModalContent
|
||||
@ -141,10 +155,16 @@ export const CreateSecretForm = ({
|
||||
subTitle="Create & update a secret across many environments"
|
||||
>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} noValidate>
|
||||
<FormControl label="Key" isRequired isError={Boolean(errors?.key)} errorText={errors?.key?.message}>
|
||||
<FormControl
|
||||
label="Key"
|
||||
isRequired
|
||||
isError={Boolean(errors?.key)}
|
||||
errorText={errors?.key?.message}
|
||||
>
|
||||
<Input
|
||||
{...register("key")}
|
||||
placeholder="Type your secret name"
|
||||
onPaste={handlePaste}
|
||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||
/>
|
||||
</FormControl>
|
||||
|
@ -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}
|
||||
|
Reference in New Issue
Block a user