Compare commits

...

67 Commits

Author SHA1 Message Date
0b1f4f0e2a fix requested changes 2024-11-26 16:35:12 +01:00
b4485a2a57 improvement: Slug Validation Errors 2024-11-26 16:35:11 +01:00
df50e3b0f9 Merge pull request #2793 from akhilmhdh/fix/signup-allow-saml
feat: resolved saml failing when signup is disabled
2024-11-26 09:39:52 -05:00
bdf2ae40b6 Merge pull request #2751 from Infisical/daniel/sap-ase-db
feat(dynamic-secrets): SAP ASE
2024-11-26 15:13:02 +04:00
960efb9cf9 docs(dynamic-secrets): SAP ASE 2024-11-26 01:54:16 +04:00
aa8d58abad feat: TDS driver docker support 2024-11-26 01:54:16 +04:00
cfb0cc4fea Update types.ts 2024-11-26 01:54:16 +04:00
7712df296c feat: SAP ASE Dynamic Secrets 2024-11-26 01:54:16 +04:00
7c38932121 fix: minor types improvement 2024-11-26 01:54:16 +04:00
69ad9845e1 improvement: added $ pattern to existing dynamic providers 2024-11-26 01:54:16 +04:00
7321c237d7 Merge pull request #2792 from Infisical/daniel/dynamic-secret-renewals
fix(dynamic-secrets): renewal 500 error
2024-11-26 01:52:28 +04:00
32430a6a16 feat: Add Project Descriptions (#2774)
* feat:  initial backend project description
2024-11-25 21:59:14 +01:00
=
f034adba76 feat: resolved saml failing when signup is disabled 2024-11-25 22:22:54 +05:30
463eb0014e fix(dynamic-secrets): renewal 500 error 2024-11-25 20:17:50 +04:00
21403f6fe5 Merge pull request #2761 from Infisical/daniel/cli-login-domains-fix
fix: allow preset domains for `infisical login`
2024-11-25 16:16:08 +04:00
2f9e542b31 Merge pull request #2760 from Infisical/daniel/request-ids
feat: request ID support
2024-11-25 16:13:19 +04:00
089d6812fd Update ldap-fns.ts 2024-11-25 16:00:20 +04:00
71c9c0fa1e Merge pull request #2781 from Infisical/daniel/project-slug-500-error
fix: improve project DAL error handling
2024-11-24 19:43:26 -05:00
2b977eeb33 fix: improve project error handling 2024-11-23 03:42:54 +04:00
a692148597 feat(integrations): Add AWS Secrets Manager IAM Role + Region (#2778) 2024-11-23 00:04:33 +01:00
64bfa4f334 Merge pull request #2779 from Infisical/fix-delete-project-role
Fix: Prevent Updating Identity/User Project Role to reserved "Custom" Slug
2024-11-22 16:23:22 -05:00
e3eb14bfd9 fix: add custom slug check to user 2024-11-22 13:09:47 -08:00
24b50651c9 fix: correct update role mapping for identity/user and prevent updating role slug to "custom" 2024-11-22 13:02:00 -08:00
1cd459fda7 Merge branch 'heads/main' into daniel/request-ids 2024-11-23 00:14:50 +04:00
38917327d9 feat: request lifecycle request ID 2024-11-22 23:19:07 +04:00
d7b494c6f8 Merge pull request #2775 from akhilmhdh/fix/patches-3
fix: db error on token auth and permission issue
2024-11-22 12:43:20 -05:00
=
93208afb36 fix: db error on token auth and permission issue 2024-11-22 22:41:53 +05:30
1a084d8fcf add direct link td provider 2024-11-21 21:26:46 -05:00
dd4f133c6c Merge pull request #2769 from Infisical/misc/made-identity-metadata-value-not-nullable-again
misc: made identity metadata value not nullable
2024-11-22 01:59:01 +08:00
c41d27e1ae misc: made identity metadata value not nullable 2024-11-21 21:27:56 +08:00
1866ed8d23 Merge pull request #2742 from Infisical/feat/totp-dynamic-secret
feat: TOTP dynamic secret provider
2024-11-21 12:00:12 +08:00
7b3b232dde replace loader with spinner 2024-11-20 14:09:26 -08:00
9d618b4ae9 minor text revisions/additions and add colors/icons to totp token expiry countdown 2024-11-20 14:01:40 -08:00
5330ab2171 Merge pull request #2768 from BnjmnZmmrmn/k8s_integration_docs_typo
fixing small typo in docs/integrations/platforms/kubernetes
2024-11-20 15:49:35 -05:00
662e588c22 misc: add handling for lease regen 2024-11-21 04:43:21 +08:00
90057d80ff Merge pull request #2767 from akhilmhdh/feat/permission-error
Detail error when permission validation error occurs
2024-11-21 02:00:51 +05:30
1eda7aaaac reverse license 2024-11-20 12:14:14 -08:00
00dcadbc08 misc: added timer 2024-11-21 04:09:19 +08:00
7a7289ebd0 fixing typo in docs/integrations/platforms/kubernetes 2024-11-20 11:50:13 -08:00
e5d4677fd6 improvements: minor UI/labeling adjustments, only show tags loading if can read, and remove rounded bottom on overview table 2024-11-20 11:50:10 -08:00
bce3f3d676 misc: addressed review comments 2024-11-21 02:37:56 +08:00
=
300372fa98 feat: resolve dependency cycle error 2024-11-20 23:59:49 +05:30
47a4f8bae9 Merge pull request #2766 from Infisical/omar/eng-1886-make-terraform-integration-secrets-marked-as-sensitive
Improvement(Terraform Cloud Integration): Synced secrets are hidden from Terraform UI
2024-11-20 13:16:45 -05:00
=
863719f296 feat: added action button for notification toast and one action each for forbidden error and validation error details 2024-11-20 22:55:14 +05:30
=
7317dc1cf5 feat: modified error handler to return possible rules for a validation failed rules 2024-11-20 22:50:21 +05:30
75df898e78 Merge pull request #2762 from Infisical/daniel/cli-installer-readme
chore(cli-installer): readme improvements
2024-11-20 20:29:23 +04:00
0de6add3f7 set all new and existing secrets to be sensitive: true 2024-11-20 17:28:09 +01:00
0c008b6393 Update README.md 2024-11-20 20:26:00 +04:00
0c3894496c feat: added support for configuring totp with secret key 2024-11-20 23:40:36 +08:00
35fbd5d49d Merge pull request #2764 from Infisical/daniel/pre-commit-cli-check
chore: check for CLI installation before pre-commit
2024-11-20 19:01:54 +04:00
d03b453e3d Merge pull request #2765 from Infisical/daniel/actor-id-mismatch
fix(audit-logs): actor / actor ID mismatch
2024-11-20 18:58:33 +04:00
96e331b678 fix(audit-logs): actor / actor ID mismatch 2024-11-20 18:50:29 +04:00
d4d468660d chore: check for CLI installation before pre-commit 2024-11-20 17:29:36 +04:00
75a4965928 requested changes 2024-11-20 16:23:59 +04:00
660c09ded4 Merge branch 'feat/totp-dynamic-secret' of https://github.com/Infisical/infisical into feat/totp-dynamic-secret 2024-11-20 18:56:56 +08:00
b5287d91c0 misc: addressed comments 2024-11-20 18:56:16 +08:00
6a17763237 docs: dynamic secret doc typos addressed 2024-11-19 19:58:01 -08:00
f2bd3daea2 Update README.md 2024-11-20 03:24:05 +04:00
7f70f96936 fix: allow preset domains for infisical login 2024-11-20 01:06:18 +04:00
73e0a54518 feat: request ID support 2024-11-20 00:01:25 +04:00
0d295a2824 fix: application crash on zod api error 2024-11-20 00:00:30 +04:00
9a62efea4f Merge pull request #2759 from Infisical/docs-update-note
update docs note
2024-11-19 23:51:44 +04:00
3be3d807d2 misc: added URL string validation 2024-11-18 19:32:57 +08:00
9f7ea3c4e5 doc: added docs for totp dynamic secret 2024-11-18 19:27:45 +08:00
e67218f170 misc: finalized option setting logic 2024-11-18 18:34:27 +08:00
269c40c67c Merge remote-tracking branch 'origin/main' into feat/totp-dynamic-secret 2024-11-18 17:31:19 +08:00
ba1fd8a3f7 feat: totp dynamic secret 2024-11-16 02:48:28 +08:00
167 changed files with 3771 additions and 1433 deletions

View File

@ -1,6 +1,12 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Check if infisical is installed
if ! command -v infisical >/dev/null 2>&1; then
echo "\nError: Infisical CLI is not installed. Please install the Infisical CLI before comitting.\n You can refer to the documentation at https://infisical.com/docs/cli/overview\n\n"
exit 1
fi
npx lint-staged
infisical scan git-changes --staged -v

View File

@ -69,13 +69,21 @@ RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
WORKDIR /app
# Required for pkcs11js
# Required for pkcs11js and ODBC
RUN apt-get update && apt-get install -y \
python3 \
make \
g++ \
unixodbc \
unixodbc-dev \
freetds-dev \
freetds-bin \
tdsodbc \
&& rm -rf /var/lib/apt/lists/*
# Configure ODBC
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
COPY backend/package*.json ./
RUN npm ci --only-production
@ -91,13 +99,21 @@ ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
WORKDIR /app
# Required for pkcs11js
# Required for pkcs11js and ODBC
RUN apt-get update && apt-get install -y \
python3 \
make \
g++ \
unixodbc \
unixodbc-dev \
freetds-dev \
freetds-bin \
tdsodbc \
&& rm -rf /var/lib/apt/lists/*
# Configure ODBC
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
COPY backend/package*.json ./
RUN npm ci --only-production
@ -108,13 +124,24 @@ RUN mkdir frontend-build
# Production stage
FROM base AS production
# Install necessary packages
# Install necessary packages including ODBC
RUN apt-get update && apt-get install -y \
ca-certificates \
curl \
git \
python3 \
make \
g++ \
unixodbc \
unixodbc-dev \
freetds-dev \
freetds-bin \
tdsodbc \
&& rm -rf /var/lib/apt/lists/*
# Configure ODBC in production
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
# Install Infisical CLI
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
&& apt-get update && apt-get install -y infisical=0.31.1 \

View File

@ -72,8 +72,16 @@ RUN addgroup --system --gid 1001 nodejs \
WORKDIR /app
# Required for pkcs11js
RUN apk add --no-cache python3 make g++
# Install all required dependencies for build
RUN apk --update add \
python3 \
make \
g++ \
unixodbc \
freetds \
unixodbc-dev \
libc-dev \
freetds-dev
COPY backend/package*.json ./
RUN npm ci --only-production
@ -88,8 +96,19 @@ FROM base AS backend-runner
WORKDIR /app
# Required for pkcs11js
RUN apk add --no-cache python3 make g++
# Install all required dependencies for runtime
RUN apk --update add \
python3 \
make \
g++ \
unixodbc \
freetds \
unixodbc-dev \
libc-dev \
freetds-dev
# Configure ODBC
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
COPY backend/package*.json ./
RUN npm ci --only-production
@ -100,11 +119,32 @@ RUN mkdir frontend-build
# Production stage
FROM base AS production
RUN apk add --upgrade --no-cache ca-certificates
RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.31.1 && apk add --no-cache git
WORKDIR /
# Install all required runtime dependencies
RUN apk --update add \
python3 \
make \
g++ \
unixodbc \
freetds \
unixodbc-dev \
libc-dev \
freetds-dev \
bash \
curl \
git
# Configure ODBC in production
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
# Setup user permissions
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user
@ -127,7 +167,6 @@ ARG CAPTCHA_SITE_KEY
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
WORKDIR /
COPY --from=backend-runner /app /backend
@ -149,4 +188,4 @@ EXPOSE 443
USER non-root-user
CMD ["./standalone-entrypoint.sh"]
CMD ["./standalone-entrypoint.sh"]

View File

@ -9,6 +9,15 @@ RUN apk --update add \
make \
g++
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
RUN apk add --no-cache \
unixodbc \
freetds \
unixodbc-dev \
libc-dev \
freetds-dev
COPY package*.json ./
RUN npm ci --only-production
@ -28,6 +37,17 @@ RUN apk --update add \
make \
g++
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
RUN apk add --no-cache \
unixodbc \
freetds \
unixodbc-dev \
libc-dev \
freetds-dev
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
RUN npm ci --only-production && npm cache clean --force
COPY --from=build /app .

View File

@ -7,7 +7,7 @@ ARG SOFTHSM2_VERSION=2.5.0
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
SOFTHSM2_SOURCES=/tmp/softhsm2
# install build dependencies including python3
# install build dependencies including python3 (required for pkcs11js and partially TDS driver)
RUN apk --update add \
alpine-sdk \
autoconf \
@ -19,7 +19,19 @@ RUN apk --update add \
make \
g++
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
RUN apk add --no-cache \
unixodbc \
freetds \
unixodbc-dev \
libc-dev \
freetds-dev
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
# build and install SoftHSM2
RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES}
WORKDIR ${SOFTHSM2_SOURCES}

View File

@ -24,6 +24,7 @@
"@fastify/multipart": "8.3.0",
"@fastify/passport": "^2.4.0",
"@fastify/rate-limit": "^9.0.0",
"@fastify/request-context": "^5.1.0",
"@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
@ -80,6 +81,7 @@
"mysql2": "^3.9.8",
"nanoid": "^3.3.4",
"nodemailer": "^6.9.9",
"odbc": "^2.4.9",
"openid-client": "^5.6.5",
"ora": "^7.0.1",
"oracledb": "^6.4.0",
@ -5528,6 +5530,15 @@
"toad-cache": "^3.3.0"
}
},
"node_modules/@fastify/request-context": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@fastify/request-context/-/request-context-5.1.0.tgz",
"integrity": "sha512-PM7wrLJOEylVDpxabOFLaYsdAiaa0lpDUcP2HMFJ1JzgiWuC6k4r3duf6Pm9YLnzlGmT+Yp4tkQjqsu7V/pSOA==",
"license": "MIT",
"dependencies": {
"fastify-plugin": "^4.0.0"
}
},
"node_modules/@fastify/send": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz",
@ -17862,6 +17873,27 @@
"jsonwebtoken": "^9.0.2"
}
},
"node_modules/odbc": {
"version": "2.4.9",
"resolved": "https://registry.npmjs.org/odbc/-/odbc-2.4.9.tgz",
"integrity": "sha512-sHFWOKfyj4oFYds7YBlN+fq9ZjC2J6CsCN5CNMABpKLp+NZdb8bnanb57OaoDy1VFXEOTE91S+F900J/aIPu6w==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.5",
"async": "^3.0.1",
"node-addon-api": "^3.0.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/odbc/node_modules/node-addon-api": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==",
"license": "MIT"
},
"node_modules/oidc-token-hash": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",

View File

@ -132,6 +132,7 @@
"@fastify/multipart": "8.3.0",
"@fastify/passport": "^2.4.0",
"@fastify/rate-limit": "^9.0.0",
"@fastify/request-context": "^5.1.0",
"@fastify/session": "^10.7.0",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
@ -188,6 +189,7 @@
"mysql2": "^3.9.8",
"nanoid": "^3.3.4",
"nodemailer": "^6.9.9",
"odbc": "^2.4.9",
"openid-client": "^5.6.5",
"ora": "^7.0.1",
"oracledb": "^6.4.0",

View File

@ -0,0 +1,7 @@
import "@fastify/request-context";
declare module "@fastify/request-context" {
interface RequestContextData {
requestId: string;
}
}

View File

@ -1,6 +1,6 @@
import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
import { Logger } from "pino";
import { CustomLogger } from "@app/lib/logger/logger";
import { ZodTypeProvider } from "@app/server/plugins/fastify-zod";
declare global {
@ -8,7 +8,7 @@ declare global {
RawServerDefault,
RawRequestDefaultExpression<RawServerDefault>,
RawReplyDefaultExpression<RawServerDefault>,
Readonly<Logger>,
Readonly<CustomLogger>,
ZodTypeProvider
>;

View File

@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasProjectDescription = await knex.schema.hasColumn(TableName.Project, "description");
if (!hasProjectDescription) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.string("description");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasProjectDescription = await knex.schema.hasColumn(TableName.Project, "description");
if (hasProjectDescription) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.dropColumn("description");
});
}
}

View File

@ -0,0 +1,20 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) {
await knex(TableName.IdentityMetadata).whereNull("value").delete();
await knex.schema.alterTable(TableName.IdentityMetadata, (t) => {
t.string("value", 1020).notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.IdentityMetadata, "value")) {
await knex.schema.alterTable(TableName.IdentityMetadata, (t) => {
t.string("value", 1020).alter();
});
}
}

View File

@ -12,7 +12,7 @@ import { TImmutableDBKeys } from "./models";
export const KmsRootConfigSchema = z.object({
id: z.string().uuid(),
encryptedRootKey: zodBuffer,
encryptionStrategy: z.string(),
encryptionStrategy: z.string().default("SOFTWARE").nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});

View File

@ -23,7 +23,8 @@ export const ProjectsSchema = z.object({
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
auditLogsRetentionDays: z.number().nullable().optional(),
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional()
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
description: z.string().nullable().optional()
});
export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@ -1,4 +1,3 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms";
import { z } from "zod";
@ -8,6 +7,7 @@ import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
@ -48,15 +48,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
.nullable(),
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
name: z
.string()
.describe(DYNAMIC_SECRETS.CREATE.name)
.min(1)
.toLowerCase()
.max(64)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
})
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name)
}),
response: {
200: z.object({

View File

@ -1,8 +1,8 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas";
import { GROUPS } from "@app/lib/api-docs";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -14,15 +14,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
schema: {
body: z.object({
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
slug: z
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(GROUPS.CREATE.slug),
slug: slugSchema({ min: 5, max: 36 }).optional().describe(GROUPS.CREATE.slug),
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(GROUPS.CREATE.role)
}),
response: {
@ -100,14 +92,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
body: z
.object({
name: z.string().trim().min(1).describe(GROUPS.UPDATE.name),
slug: z
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(GROUPS.UPDATE.slug),
slug: slugSchema({ min: 5, max: 36 }).describe(GROUPS.UPDATE.slug),
role: z.string().trim().min(1).describe(GROUPS.UPDATE.role)
})
.partial(),

View File

@ -8,6 +8,7 @@ import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import {
ProjectPermissionSchema,
@ -33,17 +34,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
body: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionSchema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.optional(),
@ -77,7 +68,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
slug: req.body.slug ?? slugify(alphaNumericNanoId(12)),
isTemporary: false,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
@ -103,17 +94,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
body: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionSchema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.optional(),
@ -159,7 +140,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
slug: req.body.slug ?? slugify(alphaNumericNanoId(12)),
isTemporary: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts
@ -189,16 +170,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.projectSlug),
privilegeDetails: z
.object({
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.privilegePermission

View File

@ -1,8 +1,8 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -18,17 +18,10 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
organizationId: z.string().trim()
}),
body: z.object({
slug: z
.string()
.min(1)
.trim()
.refine(
(val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
}),
slug: slugSchema({ min: 1, max: 64 }).refine(
(val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
),
name: z.string().trim(),
description: z.string().trim().optional(),
permissions: z.any().array()
@ -94,17 +87,13 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
roleId: z.string().trim()
}),
body: z.object({
slug: z
.string()
.trim()
.optional()
// TODO: Switch to slugSchema after verifying correct methods with Akhil - Omar 11/24
slug: slugSchema({ min: 1, max: 64 })
.refine(
(val) => typeof val !== "undefined" && !Object.keys(OrgMembershipRole).includes(val),
(val) => !Object.keys(OrgMembershipRole).includes(val),
"Please choose a different slug, the slug you have entered is reserved."
)
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
message: "Slug must be a valid"
}),
.optional(),
name: z.string().trim().optional(),
description: z.string().trim().optional(),
permissions: z.any().array().optional()

View File

@ -1,5 +1,4 @@
import { packRules } from "@casl/ability/extra";
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
@ -9,6 +8,7 @@ import {
} from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
@ -32,18 +32,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
}),
body: z.object({
slug: z
.string()
.toLowerCase()
.trim()
.min(1)
slug: slugSchema({ max: 64 })
.refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
})
.describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
@ -94,21 +87,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
}),
body: z.object({
slug: z
.string()
.toLowerCase()
.trim()
.optional()
.describe(PROJECT_ROLE.UPDATE.slug)
slug: slugSchema()
.refine(
(val) =>
typeof val === "undefined" ||
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
message: "Slug must be a valid"
}),
.describe(PROJECT_ROLE.UPDATE.slug)
.optional(),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()

View File

@ -1,4 +1,3 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
@ -8,22 +7,13 @@ import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-tem
import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
import { ProjectTemplates } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { AuthMode } from "@app/services/auth/auth-type";
const MAX_JSON_SIZE_LIMIT_IN_BYTES = 32_768;
const SlugSchema = z
.string()
.trim()
.min(1)
.max(32)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Must be valid slug format"
});
const isReservedRoleSlug = (slug: string) =>
Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
@ -34,14 +24,14 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
roles: z
.object({
name: z.string().trim().min(1),
slug: SlugSchema,
slug: slugSchema(),
permissions: UnpackedPermissionSchema.array()
})
.array(),
environments: z
.object({
name: z.string().trim().min(1),
slug: SlugSchema,
slug: slugSchema(),
position: z.number().min(1)
})
.array()
@ -50,7 +40,7 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
const ProjectTemplateRolesSchema = z
.object({
name: z.string().trim().min(1),
slug: SlugSchema,
slug: slugSchema(),
permissions: ProjectPermissionV2Schema.array()
})
.array()
@ -78,7 +68,7 @@ const ProjectTemplateRolesSchema = z
const ProjectTemplateEnvironmentsSchema = z
.object({
name: z.string().trim().min(1),
slug: SlugSchema,
slug: slugSchema(),
position: z.number().min(1)
})
.array()
@ -188,9 +178,11 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
schema: {
description: "Create a project template.",
body: z.object({
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
}).describe(ProjectTemplates.CREATE.name),
name: slugSchema({ field: "name" })
.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
})
.describe(ProjectTemplates.CREATE.name),
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
@ -230,9 +222,10 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
description: "Update a project template.",
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }),
body: z.object({
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
})
name: slugSchema({ field: "name" })
.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
})
.optional()
.describe(ProjectTemplates.UPDATE.name),
description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description),

View File

@ -7,6 +7,7 @@ import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/pr
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/santizedSchemas/user-additional-privilege";
import { AuthMode } from "@app/services/auth/auth-type";
@ -21,17 +22,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
schema: {
body: z.object({
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
type: z.discriminatedUnion("isTemporary", [
z.object({
@ -87,15 +78,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
}),
body: z
.object({
slug: z
.string()
.max(60)
.trim()
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array()
.optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),

View File

@ -7,6 +7,7 @@ import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-p
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/santizedSchemas/identitiy-additional-privilege";
import { AuthMode } from "@app/services/auth/auth-type";
@ -28,17 +29,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
body: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission),
type: z.discriminatedUnion("isTemporary", [
z.object({
@ -100,16 +91,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
id: z.string().trim().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.id)
}),
body: z.object({
slug: z
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array()
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission),

View File

@ -1,11 +1,11 @@
import { packRules } from "@casl/ability/extra";
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
@ -29,18 +29,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim().describe(PROJECT_ROLE.CREATE.projectId)
}),
body: z.object({
slug: z
.string()
.toLowerCase()
.trim()
.min(1)
slug: slugSchema({ min: 1, max: 64 })
.refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
})
.describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
@ -90,21 +83,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
}),
body: z.object({
slug: z
.string()
.toLowerCase()
.trim()
.optional()
.describe(PROJECT_ROLE.UPDATE.slug)
slug: slugSchema({ min: 1, max: 64 })
.refine(
(val) =>
typeof val === "undefined" ||
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
message: "Slug must be a valid"
}),
.optional()
.describe(PROJECT_ROLE.UPDATE.slug),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()

View File

@ -80,7 +80,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin
}
};
const addUserToInfisicalGroup = async (userId: string) => {
const $addUserToInfisicalGroup = async (userId: string) => {
// figure out if the default user is already in the group, if it is, then we shouldn't add it again
const addUserToGroupCommand = new ModifyUserGroupCommand({
@ -96,7 +96,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin
await ensureInfisicalGroupExists(clusterName);
await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user
await addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists()
await $addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists()
return {
userId: creationInput.UserId,
@ -212,7 +212,7 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
// No renewal necessary
return { entityId };
};

View File

@ -33,7 +33,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>) => {
const client = new IAMClient({
region: providerInputs.region,
credentials: {
@ -47,7 +47,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const isConnected = await client.send(new GetUserCommand({})).then(() => true);
return isConnected;
@ -55,7 +55,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const username = generateUsername();
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
@ -118,7 +118,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const username = entityId;
@ -179,9 +179,8 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
};
const renew = async (_inputs: unknown, entityId: string) => {
// do nothing
const username = entityId;
return { entityId: username };
// No renewal necessary
return { entityId };
};
return {

View File

@ -23,7 +23,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
return providerInputs;
};
const getToken = async (
const $getToken = async (
tenantId: string,
applicationId: string,
clientSecret: string
@ -51,18 +51,13 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
return data.success;
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
return { entityId };
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const data = await getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
const data = await $getToken(providerInputs.tenantId, providerInputs.applicationId, providerInputs.clientSecret);
if (!data.success) {
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
}
@ -98,7 +93,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
};
const fetchAzureEntraIdUsers = async (tenantId: string, applicationId: string, clientSecret: string) => {
const data = await getToken(tenantId, applicationId, clientSecret);
const data = await $getToken(tenantId, applicationId, clientSecret);
if (!data.success) {
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
}
@ -127,6 +122,11 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
return users;
};
const renew = async (inputs: unknown, entityId: string) => {
// No renewal necessary
return { entityId };
};
return {
validateProviderInputs,
validateConnection,

View File

@ -27,7 +27,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema>) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema>) => {
const sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
const client = new cassandra.Client({
sslOptions,
@ -47,7 +47,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
await client.shutdown();
@ -56,7 +56,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
@ -82,7 +82,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const username = entityId;
const { keyspace } = providerInputs;
@ -99,20 +99,24 @@ export const CassandraProvider = (): TDynamicProviderFns => {
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
if (!providerInputs.renewStatement) return { entityId };
const client = await $getClient(providerInputs);
const username = entityId;
const expiration = new Date(expireAt).toISOString();
const { keyspace } = providerInputs;
const renewStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace, expiration });
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
username: entityId,
keyspace,
expiration
});
const queries = renewStatement.toString().split(";").filter(Boolean);
for (const query of queries) {
// eslint-disable-next-line
for await (const query of queries) {
await client.execute(query);
}
await client.shutdown();
return { entityId: username };
return { entityId };
};
return {

View File

@ -24,7 +24,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
const connection = new ElasticSearchClient({
node: {
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
@ -55,7 +55,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const connection = await $getClient(providerInputs);
const infoResponse = await connection
.info()
@ -67,7 +67,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const connection = await $getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
@ -85,7 +85,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const connection = await $getClient(providerInputs);
await connection.security.deleteUser({
username: entityId
@ -96,7 +96,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
// No renewal necessary
return { entityId };
};

View File

@ -6,15 +6,17 @@ import { AzureEntraIDProvider } from "./azure-entra-id";
import { CassandraProvider } from "./cassandra";
import { ElasticSearchProvider } from "./elastic-search";
import { LdapProvider } from "./ldap";
import { DynamicSecretProviders } from "./models";
import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
import { MongoAtlasProvider } from "./mongo-atlas";
import { MongoDBProvider } from "./mongo-db";
import { RabbitMqProvider } from "./rabbit-mq";
import { RedisDatabaseProvider } from "./redis";
import { SapAseProvider } from "./sap-ase";
import { SapHanaProvider } from "./sap-hana";
import { SqlDatabaseProvider } from "./sql-database";
import { TotpProvider } from "./totp";
export const buildDynamicSecretProviders = () => ({
export const buildDynamicSecretProviders = (): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
@ -27,5 +29,7 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
[DynamicSecretProviders.Ldap]: LdapProvider(),
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
[DynamicSecretProviders.Snowflake]: SnowflakeProvider()
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
[DynamicSecretProviders.Totp]: TotpProvider(),
[DynamicSecretProviders.SapAse]: SapAseProvider()
});

View File

@ -52,7 +52,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => {
const $getClient = async (providerInputs: z.infer<typeof LdapSchema>): Promise<ldapjs.Client> => {
return new Promise((resolve, reject) => {
const client = ldapjs.createClient({
url: providerInputs.url,
@ -83,7 +83,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
return client.connected;
};
@ -191,7 +191,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
if (providerInputs.credentialType === LdapCredentialType.Static) {
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
@ -235,7 +235,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
if (providerInputs.credentialType === LdapCredentialType.Static) {
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
@ -268,7 +268,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
// No renewal necessary
return { entityId };
};

View File

@ -4,7 +4,8 @@ export enum SqlProviders {
Postgres = "postgres",
MySQL = "mysql2",
Oracle = "oracledb",
MsSQL = "mssql"
MsSQL = "mssql",
SapAse = "sap-ase"
}
export enum ElasticSearchAuthTypes {
@ -17,6 +18,17 @@ export enum LdapCredentialType {
Static = "static"
}
export enum TotpConfigType {
URL = "url",
MANUAL = "manual"
}
export enum TotpAlgorithm {
SHA1 = "sha1",
SHA256 = "sha256",
SHA512 = "sha512"
}
export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
@ -107,6 +119,16 @@ export const DynamicSecretCassandraSchema = z.object({
ca: z.string().optional()
});
export const DynamicSecretSapAseSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
database: z.string().trim(),
username: z.string().trim(),
password: z.string().trim(),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim()
});
export const DynamicSecretAwsIamSchema = z.object({
accessKey: z.string().trim().min(1),
secretAccessKey: z.string().trim().min(1),
@ -221,6 +243,34 @@ export const LdapSchema = z.union([
})
]);
export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [
z.object({
configType: z.literal(TotpConfigType.URL),
url: z
.string()
.url()
.trim()
.min(1)
.refine((val) => {
const urlObj = new URL(val);
const secret = urlObj.searchParams.get("secret");
return Boolean(secret);
}, "OTP URL must contain secret field")
}),
z.object({
configType: z.literal(TotpConfigType.MANUAL),
secret: z
.string()
.trim()
.min(1)
.transform((val) => val.replace(/\s+/g, "")),
period: z.number().optional(),
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
digits: z.number().optional()
})
]);
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",
Cassandra = "cassandra",
@ -234,12 +284,15 @@ export enum DynamicSecretProviders {
AzureEntraID = "azure-entra-id",
Ldap = "ldap",
SapHana = "sap-hana",
Snowflake = "snowflake"
Snowflake = "snowflake",
Totp = "totp",
SapAse = "sap-ase"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
z.object({ type: z.literal(DynamicSecretProviders.SapAse), inputs: DynamicSecretSapAseSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.SapHana), inputs: DynamicSecretSapHanaSchema }),
@ -250,7 +303,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema })
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema })
]);
export type TDynamicProviderFns = {

View File

@ -22,7 +22,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoAtlasSchema>) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoAtlasSchema>) => {
const client = axios.create({
baseURL: "https://cloud.mongodb.com/api/atlas",
headers: {
@ -40,7 +40,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const isConnected = await client({
method: "GET",
@ -59,7 +59,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
@ -87,7 +87,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const username = entityId;
const isExisting = await client({
@ -114,7 +114,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const username = entityId;
const expiration = new Date(expireAt).toISOString();

View File

@ -23,7 +23,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
const isSrv = !providerInputs.port;
const uri = isSrv
? `mongodb+srv://${providerInputs.host}`
@ -42,7 +42,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const isConnected = await client
.db(providerInputs.database)
@ -55,7 +55,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
@ -74,7 +74,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const username = entityId;
@ -88,6 +88,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
};
const renew = async (_inputs: unknown, entityId: string) => {
// No renewal necessary
return { entityId };
};

View File

@ -84,7 +84,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
const axiosInstance = axios.create({
baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`,
auth: {
@ -105,7 +105,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const connection = await $getClient(providerInputs);
const infoResponse = await connection.get("/whoami").then(() => true);
@ -114,7 +114,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const connection = await $getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
@ -134,7 +134,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const connection = await $getClient(providerInputs);
await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
@ -142,7 +142,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
// No renewal necessary
return { entityId };
};

View File

@ -55,7 +55,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
let connection: Redis | null = null;
try {
connection = new Redis({
@ -92,7 +92,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const connection = await $getClient(providerInputs);
const pingResponse = await connection
.ping()
@ -104,7 +104,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const connection = await $getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
@ -126,7 +126,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const connection = await $getClient(providerInputs);
const username = entityId;
@ -141,7 +141,9 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
if (!providerInputs.renewStatement) return { entityId };
const connection = await $getClient(providerInputs);
const username = entityId;
const expiration = new Date(expireAt).toISOString();

View File

@ -0,0 +1,145 @@
import handlebars from "handlebars";
import { customAlphabet } from "nanoid";
import odbc from "odbc";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSapAseSchema, TDynamicProviderFns } from "./models";
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return alphaNumericNanoId(25);
};
enum SapCommands {
CreateLogin = "sp_addlogin",
DropLogin = "sp_droplogin"
}
export const SapAseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs);
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapAseSchema>, useMaster?: boolean) => {
const connectionString =
`DRIVER={FreeTDS};` +
`SERVER=${providerInputs.host};` +
`PORT=${providerInputs.port};` +
`DATABASE=${useMaster ? "master" : providerInputs.database};` +
`UID=${providerInputs.username};` +
`PWD=${providerInputs.password};` +
`TDS_VERSION=5.0`;
const client = await odbc.connect(connectionString);
return client;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const masterClient = await $getClient(providerInputs, true);
const client = await $getClient(providerInputs);
const [resultFromMasterDatabase] = await masterClient.query<{ version: string }>("SELECT @@VERSION AS version");
const [resultFromSelectedDatabase] = await client.query<{ version: string }>("SELECT @@VERSION AS version");
if (!resultFromSelectedDatabase.version) {
throw new BadRequestError({
message: "Failed to validate SAP ASE connection, version query failed"
});
}
if (resultFromMasterDatabase.version !== resultFromSelectedDatabase.version) {
throw new BadRequestError({
message: "Failed to validate SAP ASE connection (master), version mismatch"
});
}
return true;
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const username = `inf_${generateUsername()}`;
const password = `${generatePassword()}`;
const client = await $getClient(providerInputs);
const masterClient = await $getClient(providerInputs, true);
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password
});
const queries = creationStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
for await (const query of queries) {
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
// If not done, then the newly created user won't be able to authenticate.
await (query.startsWith(SapCommands.CreateLogin) ? masterClient : client).query(query);
}
await masterClient.close();
await client.close();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, username: string) => {
const providerInputs = await validateProviderInputs(inputs);
const revokeStatement = handlebars.compile(providerInputs.revocationStatement, { noEscape: true })({
username
});
const queries = revokeStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
const client = await $getClient(providerInputs);
const masterClient = await $getClient(providerInputs, true);
// Get all processes for this login and kill them. If there are active connections to the database when drop login happens, it will throw an error.
const result = await masterClient.query<{ spid?: string }>(`sp_who '${username}'`);
if (result && result.length > 0) {
for await (const row of result) {
if (row.spid) {
await masterClient.query(`KILL ${row.spid.trim()}`);
}
}
}
for await (const query of queries) {
await (query.startsWith(SapCommands.DropLogin) ? masterClient : client).query(query);
}
await masterClient.close();
await client.close();
return { entityId: username };
};
const renew = async (_: unknown, username: string) => {
// No need for renewal
return { entityId: username };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@ -32,7 +32,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
const client = hdb.createClient({
host: providerInputs.host,
port: providerInputs.port,
@ -64,9 +64,9 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const testResult: boolean = await new Promise((resolve, reject) => {
const testResult = await new Promise<boolean>((resolve, reject) => {
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
if (err) {
reject();
@ -86,7 +86,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
const password = generatePassword();
const expiration = new Date(expireAt).toISOString();
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password,
@ -114,7 +114,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
const revoke = async (inputs: unknown, username: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
const queries = revokeStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
@ -135,13 +135,15 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
return { entityId: username };
};
const renew = async (inputs: unknown, username: string, expireAt: number) => {
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
if (!providerInputs.renewStatement) return { entityId };
const client = await $getClient(providerInputs);
try {
const expiration = new Date(expireAt).toISOString();
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username: entityId, expiration });
const queries = renewStatement.toString().split(";").filter(Boolean);
for await (const query of queries) {
await new Promise((resolve, reject) => {
@ -161,7 +163,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
client.disconnect();
}
return { entityId: username };
return { entityId };
};
return {

View File

@ -34,7 +34,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSnowflakeSchema>) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSnowflakeSchema>) => {
const client = snowflake.createConnection({
account: `${providerInputs.orgId}-${providerInputs.accountId}`,
username: providerInputs.username,
@ -49,7 +49,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
let isValidConnection: boolean;
@ -72,7 +72,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
@ -107,7 +107,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
const revoke = async (inputs: unknown, username: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
try {
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
@ -131,17 +131,16 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
return { entityId: username };
};
const renew = async (inputs: unknown, username: string, expireAt: number) => {
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
if (!providerInputs.renewStatement) return { entityId };
if (!providerInputs.renewStatement) return { entityId: username };
const client = await getClient(providerInputs);
const client = await $getClient(providerInputs);
try {
const expiration = getDaysToExpiry(new Date(expireAt));
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
username,
username: entityId,
expiration
});
@ -161,7 +160,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
client.destroy(noop);
}
return { entityId: username };
return { entityId };
};
return {

View File

@ -32,7 +32,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
const ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
const db = knex({
client: providerInputs.client,
@ -52,7 +52,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const db = await getClient(providerInputs);
const db = await $getClient(providerInputs);
// oracle needs from keyword
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
@ -63,7 +63,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const db = await getClient(providerInputs);
const db = await $getClient(providerInputs);
const username = generateUsername(providerInputs.client);
const password = generatePassword(providerInputs.client);
@ -90,7 +90,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const db = await getClient(providerInputs);
const db = await $getClient(providerInputs);
const username = entityId;
const { database } = providerInputs;
@ -110,13 +110,19 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const db = await getClient(providerInputs);
if (!providerInputs.renewStatement) return { entityId };
const db = await $getClient(providerInputs);
const username = entityId;
const expiration = new Date(expireAt).toISOString();
const { database } = providerInputs;
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database });
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
username: entityId,
expiration,
database
});
if (renewStatement) {
const queries = renewStatement.toString().split(";").filter(Boolean);
await db.transaction(async (tx) => {
@ -128,7 +134,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
}
await db.destroy();
return { entityId: username };
return { entityId };
};
return {

View File

@ -0,0 +1,90 @@
import { authenticator } from "otplib";
import { HashAlgorithms } from "otplib/core";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models";
export const TotpProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretTotpSchema.parseAsync(inputs);
return providerInputs;
};
const validateConnection = async () => {
return true;
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const entityId = alphaNumericNanoId(32);
const authenticatorInstance = authenticator.clone();
let secret: string;
let period: number | null | undefined;
let digits: number | null | undefined;
let algorithm: HashAlgorithms | null | undefined;
if (providerInputs.configType === TotpConfigType.URL) {
const urlObj = new URL(providerInputs.url);
secret = urlObj.searchParams.get("secret") as string;
const periodFromUrl = urlObj.searchParams.get("period");
const digitsFromUrl = urlObj.searchParams.get("digits");
const algorithmFromUrl = urlObj.searchParams.get("algorithm");
if (periodFromUrl) {
period = +periodFromUrl;
}
if (digitsFromUrl) {
digits = +digitsFromUrl;
}
if (algorithmFromUrl) {
algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms;
}
} else {
secret = providerInputs.secret;
period = providerInputs.period;
digits = providerInputs.digits;
algorithm = providerInputs.algorithm as unknown as HashAlgorithms;
}
if (digits) {
authenticatorInstance.options = { digits };
}
if (algorithm) {
authenticatorInstance.options = { algorithm };
}
if (period) {
authenticatorInstance.options = { step: period };
}
return {
entityId,
data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() }
};
};
const revoke = async (_inputs: unknown, entityId: string) => {
return { entityId };
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const renew = async (_inputs: unknown, entityId: string) => {
// No renewal necessary
return { entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@ -27,7 +27,7 @@ export const initializeHsmModule = () => {
logger.info("PKCS#11 module initialized");
} catch (err) {
logger.error("Failed to initialize PKCS#11 module:", err);
logger.error(err, "Failed to initialize PKCS#11 module");
throw err;
}
};
@ -39,7 +39,7 @@ export const initializeHsmModule = () => {
isInitialized = false;
logger.info("PKCS#11 module finalized");
} catch (err) {
logger.error("Failed to finalize PKCS#11 module:", err);
logger.error(err, "Failed to finalize PKCS#11 module");
throw err;
}
}

View File

@ -36,8 +36,7 @@ export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise<boolean>
});
ldapClient.on("error", (err) => {
logger.error("LDAP client error:", err);
logger.error(err);
logger.error(err, "LDAP client error");
resolve(false);
});

View File

@ -161,8 +161,8 @@ export const licenseServiceFactory = ({
}
} catch (error) {
logger.error(
`getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`,
error
error,
`getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`
);
await keyStore.setItemWithExpiry(
FEATURE_CACHE_KEY(orgId),

View File

@ -127,14 +127,15 @@ export const permissionDALFactory = (db: TDbClient) => {
const getProjectPermission = async (userId: string, projectId: string) => {
try {
const subQueryUserGroups = db(TableName.UserGroupMembership).where("userId", userId).select("groupId");
const docs = await db
.replicaNode()(TableName.Users)
.where(`${TableName.Users}.id`, userId)
.leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.GroupProjectMembership, (queryBuilder) => {
void queryBuilder
.on(`${TableName.GroupProjectMembership}.projectId`, db.raw("?", [projectId]))
.andOn(`${TableName.GroupProjectMembership}.groupId`, `${TableName.UserGroupMembership}.groupId`);
// @ts-expect-error akhilmhdh: this is valid knexjs query. Its just ts type argument is missing it
.andOnIn(`${TableName.GroupProjectMembership}.groupId`, subQueryUserGroups);
})
.leftJoin(
TableName.GroupProjectMembershipRole,

View File

@ -1,14 +1,7 @@
import picomatch from "picomatch";
import { z } from "zod";
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
$REGEX = "$regex",
$EQ = "$eq",
$NEQ = "$ne",
$GLOB = "$glob"
}
import { PermissionConditionOperators } from "@app/lib/casl";
export const PermissionConditionSchema = {
[PermissionConditionOperators.$IN]: z.string().trim().min(1).array(),

View File

@ -1,10 +1,10 @@
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { z } from "zod";
import { conditionsMatcher } from "@app/lib/casl";
import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { PermissionConditionOperators, PermissionConditionSchema } from "./permission-types";
import { PermissionConditionSchema } from "./permission-types";
export enum ProjectPermissionActions {
Read = "read",

View File

@ -46,7 +46,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
}
return rateLimit;
} catch (err) {
logger.error("Error fetching rate limits %o", err);
logger.error(err, "Error fetching rate limits");
return undefined;
}
};
@ -69,12 +69,12 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
mfaRateLimit: rateLimit.mfaRateLimit
};
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);
logger.info(newRateLimitMaxConfiguration, "syncRateLimitConfiguration: rate limit configuration");
Object.freeze(newRateLimitMaxConfiguration);
rateLimitMaxConfiguration = newRateLimitMaxConfiguration;
}
} catch (error) {
logger.error(`Error syncing rate limit configurations: %o`, error);
logger.error(error, "Error syncing rate limit configurations");
}
};

View File

@ -238,11 +238,11 @@ export const secretScanningQueueFactory = ({
});
queueService.listen(QueueName.SecretPushEventScan, "failed", (job, err) => {
logger.error("Failed to secret scan on push", job?.data, err);
logger.error(err, "Failed to secret scan on push", job?.data);
});
queueService.listen(QueueName.SecretFullRepoScan, "failed", (job, err) => {
logger.error("Failed to do full repo secret scan", job?.data, err);
logger.error(err, "Failed to do full repo secret scan", job?.data);
});
return { startFullRepoScan, startPushEventScan };

View File

@ -391,6 +391,7 @@ export const PROJECTS = {
CREATE: {
organizationSlug: "The slug of the organization to create the project in.",
projectName: "The name of the project to create.",
projectDescription: "An optional description label for the project.",
slug: "An optional slug for the project.",
template: "The name of the project template, if specified, to apply to this project."
},
@ -403,6 +404,7 @@ export const PROJECTS = {
UPDATE: {
workspaceId: "The ID of the project to update.",
name: "The new name of the project.",
projectDescription: "An optional description label for the project.",
autoCapitalization: "Disable or enable auto-capitalization for the project."
},
GET_KEY: {

View File

@ -54,3 +54,12 @@ export const isAtLeastAsPrivileged = (permissions1: MongoAbility, permissions2:
return set1.size >= set2.size;
};
export enum PermissionConditionOperators {
$IN = "$in",
$ALL = "$all",
$REGEX = "$regex",
$EQ = "$eq",
$NEQ = "$ne",
$GLOB = "$glob"
}

View File

@ -1,7 +1,7 @@
import { Logger } from "pino";
import { z } from "zod";
import { removeTrailingSlash } from "../fn";
import { CustomLogger } from "../logger/logger";
import { zpStr } from "../zod";
export const GITLAB_URL = "https://gitlab.com";
@ -212,7 +212,7 @@ let envCfg: Readonly<z.infer<typeof envSchema>>;
export const getConfig = () => envCfg;
// cannot import singleton logger directly as it needs config to load various transport
export const initEnvConfig = (logger?: Logger) => {
export const initEnvConfig = (logger?: CustomLogger) => {
const parsedEnv = envSchema.safeParse(process.env);
if (!parsedEnv.success) {
(logger ?? console).error("Invalid environment variables. Check the error below");

View File

@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// logger follows a singleton pattern
// easier to use it that's all.
import { requestContext } from "@fastify/request-context";
import pino, { Logger } from "pino";
import { z } from "zod";
@ -13,14 +15,37 @@ const logLevelToSeverityLookup: Record<string, string> = {
"60": "CRITICAL"
};
// eslint-disable-next-line import/no-mutable-exports
export let logger: Readonly<Logger>;
// akhilmhdh:
// The logger is not placed in the main app config to avoid a circular dependency.
// The config requires the logger to display errors when an invalid environment is supplied.
// On the other hand, the logger needs the config to obtain credentials for AWS or other transports.
// By keeping the logger separate, it becomes an independent package.
// We define our own custom logger interface to enforce structure to the logging methods.
export interface CustomLogger extends Omit<Logger, "info" | "error" | "warn" | "debug"> {
info: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj: unknown, msg?: string, ...args: any[]): void;
};
error: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj: unknown, msg?: string, ...args: any[]): void;
};
warn: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj: unknown, msg?: string, ...args: any[]): void;
};
debug: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj: unknown, msg?: string, ...args: any[]): void;
};
}
// eslint-disable-next-line import/no-mutable-exports
export let logger: Readonly<CustomLogger>;
const loggerConfig = z.object({
AWS_CLOUDWATCH_LOG_GROUP_NAME: z.string().default("infisical-log-stream"),
AWS_CLOUDWATCH_LOG_REGION: z.string().default("us-east-1"),
@ -62,6 +87,17 @@ const redactedKeys = [
"config"
];
const UNKNOWN_REQUEST_ID = "UNKNOWN_REQUEST_ID";
const extractRequestId = () => {
try {
return requestContext.get("requestId") || UNKNOWN_REQUEST_ID;
} catch (err) {
console.log("failed to get request context", err);
return UNKNOWN_REQUEST_ID;
}
};
export const initLogger = async () => {
const cfg = loggerConfig.parse(process.env);
const targets: pino.TransportMultiOptions["targets"][number][] = [
@ -94,6 +130,30 @@ export const initLogger = async () => {
targets
});
const wrapLogger = (originalLogger: Logger): CustomLogger => {
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.info = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).info(obj, msg, ...args);
};
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.error = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).error(obj, msg, ...args);
};
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.warn = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).warn(obj, msg, ...args);
};
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.debug = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).debug(obj, msg, ...args);
};
return originalLogger;
};
logger = pino(
{
mixin(_context, level) {
@ -113,5 +173,6 @@ export const initLogger = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
transport
);
return logger;
return wrapLogger(logger);
};

View File

@ -10,13 +10,15 @@ import fastifyFormBody from "@fastify/formbody";
import helmet from "@fastify/helmet";
import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
import ratelimiter from "@fastify/rate-limit";
import { fastifyRequestContext } from "@fastify/request-context";
import fastify from "fastify";
import { Knex } from "knex";
import { Logger } from "pino";
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig, IS_PACKAGED } from "@app/lib/config/env";
import { CustomLogger } from "@app/lib/logger/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TQueueServiceFactory } from "@app/queue";
import { TSmtpService } from "@app/services/smtp/smtp-service";
@ -35,7 +37,7 @@ type TMain = {
auditLogDb?: Knex;
db: Knex;
smtp: TSmtpService;
logger?: Logger;
logger?: CustomLogger;
queue: TQueueServiceFactory;
keyStore: TKeyStoreFactory;
hsmModule: HsmModule;
@ -47,7 +49,9 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
const server = fastify({
logger: appCfg.NODE_ENV === "test" ? false : logger,
genReqId: () => `req-${alphaNumericNanoId(14)}`,
trustProxy: true,
connectionTimeout: appCfg.isHsmConfigured ? 90_000 : 30_000,
ignoreTrailingSlash: true,
pluginTimeout: 40_000
@ -104,6 +108,13 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
await server.register(maintenanceMode);
await server.register(fastifyRequestContext, {
defaultStoreValues: (request) => ({
requestId: request.id,
log: request.log.child({ requestId: request.id })
})
});
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule });
if (appCfg.isProductionMode) {

View File

@ -0,0 +1,24 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
interface SlugSchemaInputs {
min?: number;
max?: number;
field?: string;
}
export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInputs = {}) => {
return z
.string()
.toLowerCase()
.trim()
.min(min, {
message: `${field} field must be at least ${min} character${min === 1 ? "" : "s"}`
})
.max(max, {
message: `${field} field must be at most ${max} character${max === 1 ? "" : "s"}`
})
.refine((v) => slugify(v, { lowercase: true, separator: "-" }) === v, {
message: `${field} field can only contain letters, numbers, and hyphens`
});
};

View File

@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, PureAbility } from "@casl/ability";
import fastifyPlugin from "fastify-plugin";
import jwt from "jsonwebtoken";
import { ZodError } from "zod";
@ -39,77 +39,102 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
if (error instanceof BadRequestError) {
void res
.status(HttpStatusCodes.BadRequest)
.send({ statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name });
.send({ requestId: req.id, statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name });
} else if (error instanceof NotFoundError) {
void res
.status(HttpStatusCodes.NotFound)
.send({ statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name });
.send({ requestId: req.id, statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name });
} else if (error instanceof UnauthorizedError) {
void res
.status(HttpStatusCodes.Unauthorized)
.send({ statusCode: HttpStatusCodes.Unauthorized, message: error.message, error: error.name });
void res.status(HttpStatusCodes.Unauthorized).send({
requestId: req.id,
statusCode: HttpStatusCodes.Unauthorized,
message: error.message,
error: error.name
});
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
void res
.status(HttpStatusCodes.InternalServerError)
.send({ statusCode: HttpStatusCodes.InternalServerError, message: "Something went wrong", error: error.name });
void res.status(HttpStatusCodes.InternalServerError).send({
requestId: req.id,
statusCode: HttpStatusCodes.InternalServerError,
message: "Something went wrong",
error: error.name
});
} else if (error instanceof GatewayTimeoutError) {
void res
.status(HttpStatusCodes.GatewayTimeout)
.send({ statusCode: HttpStatusCodes.GatewayTimeout, message: error.message, error: error.name });
void res.status(HttpStatusCodes.GatewayTimeout).send({
requestId: req.id,
statusCode: HttpStatusCodes.GatewayTimeout,
message: error.message,
error: error.name
});
} else if (error instanceof ZodError) {
void res
.status(HttpStatusCodes.Unauthorized)
.send({ statusCode: HttpStatusCodes.Unauthorized, error: "ValidationFailure", message: error.issues });
void res.status(HttpStatusCodes.Unauthorized).send({
requestId: req.id,
statusCode: HttpStatusCodes.Unauthorized,
error: "ValidationFailure",
message: error.issues
});
} else if (error instanceof ForbiddenError) {
void res.status(HttpStatusCodes.Forbidden).send({
requestId: req.id,
statusCode: HttpStatusCodes.Forbidden,
error: "PermissionDenied",
message: `You are not allowed to ${error.action} on ${error.subjectType} - ${JSON.stringify(error.subject)}`
message: `You are not allowed to ${error.action} on ${error.subjectType}`,
details: (error.ability as PureAbility).rulesFor(error.action as string, error.subjectType).map((el) => ({
action: el.action,
inverted: el.inverted,
subject: el.subject,
conditions: el.conditions
}))
});
} else if (error instanceof ForbiddenRequestError) {
void res.status(HttpStatusCodes.Forbidden).send({
requestId: req.id,
statusCode: HttpStatusCodes.Forbidden,
message: error.message,
error: error.name
});
} else if (error instanceof RateLimitError) {
void res.status(HttpStatusCodes.TooManyRequests).send({
requestId: req.id,
statusCode: HttpStatusCodes.TooManyRequests,
message: error.message,
error: error.name
});
} else if (error instanceof ScimRequestError) {
void res.status(error.status).send({
requestId: req.id,
schemas: error.schemas,
status: error.status,
detail: error.detail
});
} else if (error instanceof OidcAuthError) {
void res
.status(HttpStatusCodes.InternalServerError)
.send({ statusCode: HttpStatusCodes.InternalServerError, message: error.message, error: error.name });
void res.status(HttpStatusCodes.InternalServerError).send({
requestId: req.id,
statusCode: HttpStatusCodes.InternalServerError,
message: error.message,
error: error.name
});
} else if (error instanceof jwt.JsonWebTokenError) {
const message = (() => {
if (error.message === JWTErrors.JwtExpired) {
return "Your token has expired. Please re-authenticate.";
}
if (error.message === JWTErrors.JwtMalformed) {
return "The provided access token is malformed. Please use a valid token or generate a new one and try again.";
}
if (error.message === JWTErrors.InvalidAlgorithm) {
return "The access token is signed with an invalid algorithm. Please provide a valid token and try again.";
}
let errorMessage = error.message;
return error.message;
})();
if (error.message === JWTErrors.JwtExpired) {
errorMessage = "Your token has expired. Please re-authenticate.";
} else if (error.message === JWTErrors.JwtMalformed) {
errorMessage =
"The provided access token is malformed. Please use a valid token or generate a new one and try again.";
} else if (error.message === JWTErrors.InvalidAlgorithm) {
errorMessage =
"The access token is signed with an invalid algorithm. Please provide a valid token and try again.";
}
void res.status(HttpStatusCodes.Forbidden).send({
requestId: req.id,
statusCode: HttpStatusCodes.Forbidden,
error: "TokenError",
message
message: errorMessage
});
} else {
void res.status(HttpStatusCodes.InternalServerError).send({
requestId: req.id,
statusCode: HttpStatusCodes.InternalServerError,
error: "InternalServerError",
message: "Something went wrong"

View File

@ -19,7 +19,7 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) =>
app.on("installation", async (context) => {
const { payload } = context;
logger.info("Installed secret scanner to:", { repositories: payload.repositories });
logger.info({ repositories: payload.repositories }, "Installed secret scanner to");
});
app.on("push", async (context) => {

View File

@ -30,26 +30,32 @@ export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
export const DefaultResponseErrorsSchema = {
400: z.object({
requestId: z.string(),
statusCode: z.literal(400),
message: z.string(),
error: z.string()
}),
404: z.object({
requestId: z.string(),
statusCode: z.literal(404),
message: z.string(),
error: z.string()
}),
401: z.object({
requestId: z.string(),
statusCode: z.literal(401),
message: z.any(),
error: z.string()
}),
403: z.object({
requestId: z.string(),
statusCode: z.literal(403),
message: z.string(),
details: z.any().optional(),
error: z.string()
}),
500: z.object({
requestId: z.string(),
statusCode: z.literal(500),
message: z.string(),
error: z.string()
@ -206,6 +212,7 @@ export const SanitizedAuditLogStreamSchema = z.object({
export const SanitizedProjectSchema = ProjectsSchema.pick({
id: true,
name: true,
description: true,
slug: true,
autoCapitalization: true,
orgId: true,

View File

@ -1,4 +1,3 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
@ -8,19 +7,12 @@ import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CmekOrderBy } from "@app/services/cmek/cmek-types";
const keyNameSchema = z
.string()
.trim()
.min(1)
.max(32)
.toLowerCase()
.refine((v) => slugify(v) === v, {
message: "Name must be slug friendly"
});
const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
const keyDescriptionSchema = z.string().trim().max(500).optional();
const base64Schema = z.string().superRefine((val, ctx) => {

View File

@ -1,9 +1,9 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ExternalGroupOrgRoleMappingsSchema } from "@app/db/schemas/external-group-org-role-mappings";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -48,13 +48,7 @@ export const registerExternalGroupOrgRoleMappingRouter = async (server: FastifyZ
mappings: z
.object({
groupName: z.string().trim().min(1),
roleSlug: z
.string()
.min(1)
.toLowerCase()
.refine((v) => slugify(v) === v, {
message: "Role must be a valid slug"
})
roleSlug: slugSchema({ max: 64 })
})
.array()
}),

View File

@ -9,6 +9,7 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
import { Integrations } from "@app/services/integration-auth/integration-list";
import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
import {} from "../sanitizedSchemas";
@ -206,6 +207,33 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
id: req.params.integrationId
});
if (integration.region) {
integration.metadata = {
...(integration.metadata || {}),
region: integration.region
};
}
if (
integration.integration === Integrations.AWS_SECRET_MANAGER ||
integration.integration === Integrations.AWS_PARAMETER_STORE
) {
const awsRoleDetails = await server.services.integration.getIntegrationAWSIamRole({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationId
});
if (awsRoleDetails) {
integration.metadata = {
...(integration.metadata || {}),
awsIamRole: awsRoleDetails.role
};
}
}
return { integration };
}
});

View File

@ -1,4 +1,3 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import {
@ -14,6 +13,7 @@ import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-t
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getLastMidnightDateISO } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
@ -243,22 +243,10 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
params: z.object({ organizationId: z.string().trim() }),
body: z.object({
name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(),
slug: z
.string()
.trim()
.max(64, { message: "Slug must be 64 or fewer characters" })
.regex(/^[a-zA-Z0-9-]+$/, "Slug must only contain alphanumeric characters or hyphens")
.optional(),
slug: slugSchema({ max: 64 }).optional(),
authEnforced: z.boolean().optional(),
scimEnabled: z.boolean().optional(),
defaultMembershipRoleSlug: z
.string()
.min(1)
.trim()
.refine((v) => slugify(v) === v, {
message: "Membership role must be a valid slug"
})
.optional(),
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
enforceMfa: z.boolean().optional(),
selectedMfaMethod: z.nativeEnum(MfaMethod).optional()
}),

View File

@ -1,10 +1,10 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ProjectEnvironmentsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ENVIRONMENTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -124,13 +124,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
body: z.object({
name: z.string().trim().describe(ENVIRONMENTS.CREATE.name),
position: z.number().min(1).optional().describe(ENVIRONMENTS.CREATE.position),
slug: z
.string()
.trim()
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(ENVIRONMENTS.CREATE.slug)
slug: slugSchema({ max: 64 }).describe(ENVIRONMENTS.CREATE.slug)
}),
response: {
200: z.object({
@ -188,14 +182,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id)
}),
body: z.object({
slug: z
.string()
.trim()
.optional()
.refine((v) => !v || slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(ENVIRONMENTS.UPDATE.slug),
slug: slugSchema({ max: 64 }).optional().describe(ENVIRONMENTS.UPDATE.slug),
name: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.name),
position: z.number().optional().describe(ENVIRONMENTS.UPDATE.position)
}),

View File

@ -296,6 +296,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.max(64, { message: "Name must be 64 or fewer characters" })
.optional()
.describe(PROJECTS.UPDATE.name),
description: z
.string()
.trim()
.max(256, { message: "Description must be 256 or fewer characters" })
.optional()
.describe(PROJECTS.UPDATE.projectDescription),
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
}),
response: {
@ -313,6 +319,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
update: {
name: req.body.name,
description: req.body.description,
autoCapitalization: req.body.autoCapitalization
},
actorAuthMethod: req.permission.authMethod,

View File

@ -1,9 +1,9 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { SecretTagsSchema } from "@app/db/schemas";
import { SECRET_TAGS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -111,14 +111,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
}),
body: z.object({
slug: z
.string()
.toLowerCase()
.trim()
.describe(SECRET_TAGS.CREATE.slug)
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.CREATE.slug),
color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
}),
response: {
@ -153,14 +146,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
}),
body: z.object({
slug: z
.string()
.toLowerCase()
.trim()
.describe(SECRET_TAGS.UPDATE.slug)
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.UPDATE.slug),
color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
}),
response: {

View File

@ -1,10 +1,10 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { SlackIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { getConfig } from "@app/lib/config/env";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -35,12 +35,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
}
],
querystring: z.object({
slug: z
.string()
.trim()
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
}),
slug: slugSchema({ max: 64 }),
description: z.string().optional()
}),
response: {
@ -288,13 +283,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
id: z.string()
}),
body: z.object({
slug: z
.string()
.trim()
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional(),
slug: slugSchema({ max: 64 }).optional(),
description: z.string().optional()
}),
response: {

View File

@ -1,4 +1,3 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import {
@ -12,6 +11,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@ -27,14 +27,6 @@ const projectWithEnv = SanitizedProjectSchema.extend({
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
});
const slugSchema = z
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be at least 5 character but no more than 36"
});
export const registerProjectRouter = async (server: FastifyZodProvider) => {
/* Get project key */
server.route({
@ -161,21 +153,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
],
body: z.object({
projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
slug: z
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(PROJECTS.CREATE.slug),
projectDescription: z.string().trim().optional().describe(PROJECTS.CREATE.projectDescription),
slug: slugSchema({ min: 5, max: 36 }).optional().describe(PROJECTS.CREATE.slug),
kmsKeyId: z.string().optional(),
template: z
.string()
.refine((v) => slugify(v) === v, {
message: "Template name must be in slug format"
})
template: slugSchema({ field: "Template Name", max: 64 })
.optional()
.default(InfisicalProjectTemplate.Default)
.describe(PROJECTS.CREATE.template)
@ -194,6 +175,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
workspaceName: req.body.projectName,
workspaceDescription: req.body.projectDescription,
slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId,
template: req.body.template
@ -242,7 +224,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
slug: slugSchema.describe("The slug of the project to delete.")
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to delete.")
}),
response: {
200: SanitizedProjectSchema
@ -276,7 +258,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
schema: {
params: z.object({
slug: slugSchema.describe("The slug of the project to get.")
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to get.")
}),
response: {
200: projectWithEnv
@ -309,11 +291,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
schema: {
params: z.object({
slug: slugSchema.describe("The slug of the project to update.")
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to update.")
}),
body: z.object({
name: z.string().trim().optional().describe("The new name of the project."),
autoCapitalization: z.boolean().optional().describe("The new auto-capitalization setting.")
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription),
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
}),
response: {
200: SanitizedProjectSchema
@ -330,6 +313,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
update: {
name: req.body.name,
description: req.body.description,
autoCapitalization: req.body.autoCapitalization
},
actorId: req.permission.id,
@ -350,7 +334,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
schema: {
params: z.object({
slug: slugSchema.describe(PROJECTS.LIST_CAS.slug)
slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CAS.slug)
}),
querystring: z.object({
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional().describe(PROJECTS.LIST_CAS.status),
@ -391,7 +375,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
schema: {
params: z.object({
slug: slugSchema.describe(PROJECTS.LIST_CERTIFICATES.slug)
slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CERTIFICATES.slug)
}),
querystring: z.object({
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),

View File

@ -119,13 +119,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
if (!userAgent) throw new Error("user agent header is required");
const appCfg = getConfig();
const serverCfg = await getServerCfg();
if (!serverCfg.allowSignUp) {
throw new ForbiddenRequestError({
message: "Signup's are disabled"
});
}
const { user, accessToken, refreshToken, organizationId } =
await server.services.signup.completeEmailAccountSignup({
...req.body,

View File

@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { NotFoundError } from "@app/lib/errors";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@ -23,6 +23,7 @@ import { TOrgServiceFactory } from "../org/org-service";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { getServerCfg } from "../super-admin/super-admin-service";
import { TUserDALFactory } from "../user/user-dal";
import { UserEncryption } from "../user/user-types";
import { TAuthDALFactory } from "./auth-dal";
@ -151,6 +152,8 @@ export const authSignupServiceFactory = ({
authorization
}: TCompleteAccountSignupDTO) => {
const appCfg = getConfig();
const serverCfg = await getServerCfg();
const user = await userDAL.findOne({ username: email });
if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user");
@ -163,6 +166,12 @@ export const authSignupServiceFactory = ({
authMethod = userAuthMethod;
organizationId = orgId;
} else {
// disallow signup if disabled. we are not doing this for providerAuthToken because we allow signups via saml or sso
if (!serverCfg.allowSignUp) {
throw new ForbiddenRequestError({
message: "Signup's are disabled"
});
}
validateSignUpAuthorization(authorization, user.id);
}

View File

@ -182,7 +182,12 @@ export const identityProjectServiceFactory = ({
// validate custom roles input
const customInputRoles = roles.filter(
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
({ role }) =>
!Object.values(ProjectMembershipRole)
// we don't want to include custom in this check;
// this unintentionally enables setting slug to custom which is reserved
.filter((r) => r !== ProjectMembershipRole.Custom)
.includes(role as ProjectMembershipRole)
);
const hasCustomRole = Boolean(customInputRoles.length);
const customRoles = hasCustomRole

View File

@ -385,8 +385,8 @@ export const identityTokenAuthServiceFactory = ({
actorOrgId
}: TUpdateTokenAuthTokenDTO) => {
const foundToken = await identityAccessTokenDAL.findOne({
id: tokenId,
authMethod: IdentityAuthMethod.TOKEN_AUTH
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
[`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH
});
if (!foundToken) throw new NotFoundError({ message: `Token with ID ${tokenId} not found` });
@ -444,8 +444,8 @@ export const identityTokenAuthServiceFactory = ({
}: TRevokeTokenAuthTokenDTO) => {
const identityAccessToken = await identityAccessTokenDAL.findOne({
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
isAccessTokenRevoked: false,
authMethod: IdentityAuthMethod.TOKEN_AUTH
[`${TableName.IdentityAccessToken}.isAccessTokenRevoked` as "isAccessTokenRevoked"]: false,
[`${TableName.IdentityAccessToken}.authMethod` as "authMethod"]: IdentityAuthMethod.TOKEN_AUTH
});
if (!identityAccessToken)
throw new NotFoundError({

View File

@ -3075,7 +3075,7 @@ const syncSecretsTerraformCloud = async ({
}) => {
// get secrets from Terraform Cloud
const terraformSecrets = (
await request.get<{ data: { attributes: { key: string; value: string }; id: string }[] }>(
await request.get<{ data: { attributes: { key: string; value: string; sensitive: boolean }; id: string }[] }>(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`,
{
headers: {
@ -3089,7 +3089,7 @@ const syncSecretsTerraformCloud = async ({
...obj,
[secret.attributes.key]: secret
}),
{} as Record<string, { attributes: { key: string; value: string }; id: string }>
{} as Record<string, { attributes: { key: string; value: string; sensitive: boolean }; id: string }>
);
const secretsToAdd: { [key: string]: string } = {};
@ -3170,7 +3170,8 @@ const syncSecretsTerraformCloud = async ({
attributes: {
key,
value: secrets[key]?.value,
category: integration.targetService
category: integration.targetService,
sensitive: true
}
}
},
@ -3183,7 +3184,11 @@ const syncSecretsTerraformCloud = async ({
}
);
// case: secret exists in Terraform Cloud
} else if (secrets[key]?.value !== terraformSecrets[key].attributes.value) {
} else if (
// we now set secrets to sensitive in Terraform Cloud, this checks if existing secrets are not sensitive and updates them accordingly
!terraformSecrets[key].attributes.sensitive ||
secrets[key]?.value !== terraformSecrets[key].attributes.value
) {
// -> update secret
await request.patch(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${terraformSecrets[key].id}`,
@ -3193,7 +3198,8 @@ const syncSecretsTerraformCloud = async ({
id: terraformSecrets[key].id,
attributes: {
...terraformSecrets[key],
value: secrets[key]?.value
value: secrets[key]?.value,
sensitive: true
}
}
},

View File

@ -9,6 +9,7 @@ import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret";
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 { TSecretQueueFactory } from "../secret/secret-queue";
@ -237,6 +238,46 @@ export const integrationServiceFactory = ({
return { ...integration, envId: integration.environment.id };
};
const getIntegrationAWSIamRole = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
const integration = await integrationDAL.findById(id);
if (!integration) {
throw new NotFoundError({
message: `Integration with ID '${id}' not found`
});
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integration?.projectId || "",
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: integration.projectId
});
let awsIamRole: string | null = null;
if (integrationAuth.encryptedAwsAssumeIamRoleArn) {
const awsAssumeRoleArn = secretManagerDecryptor({
cipherTextBlob: Buffer.from(integrationAuth.encryptedAwsAssumeIamRoleArn)
}).toString();
if (awsAssumeRoleArn) {
const [, role] = awsAssumeRoleArn.split(":role/");
awsIamRole = role;
}
}
return {
role: awsIamRole
};
};
const deleteIntegration = async ({
actorId,
id,
@ -329,6 +370,7 @@ export const integrationServiceFactory = ({
deleteIntegration,
listIntegrationByProject,
getIntegration,
getIntegrationAWSIamRole,
syncIntegration
};
};

View File

@ -280,7 +280,12 @@ export const projectMembershipServiceFactory = ({
// validate custom roles input
const customInputRoles = roles.filter(
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
({ role }) =>
!Object.values(ProjectMembershipRole)
// we don't want to include custom in this check;
// this unintentionally enables setting slug to custom which is reserved
.filter((r) => r !== ProjectMembershipRole.Custom)
.includes(role as ProjectMembershipRole)
);
const hasCustomRole = Boolean(customInputRoles.length);
if (hasCustomRole) {

View File

@ -191,6 +191,10 @@ export const projectDALFactory = (db: TDbClient) => {
return project;
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
throw new DatabaseError({ error, name: "Find all projects" });
}
};
@ -240,6 +244,10 @@ export const projectDALFactory = (db: TDbClient) => {
return project;
} catch (error) {
if (error instanceof NotFoundError || error instanceof UnauthorizedError) {
throw error;
}
throw new DatabaseError({ error, name: "Find project by slug" });
}
};
@ -260,7 +268,7 @@ export const projectDALFactory = (db: TDbClient) => {
}
throw new BadRequestError({ message: "Invalid filter type" });
} catch (error) {
if (error instanceof BadRequestError) {
if (error instanceof BadRequestError || error instanceof NotFoundError || error instanceof UnauthorizedError) {
throw error;
}
throw new DatabaseError({ error, name: `Failed to find project by ${filter.type}` });

View File

@ -285,11 +285,14 @@ export const projectQueueFactory = ({
if (!orgMembership) {
// This can happen. Since we don't remove project memberships and project keys when a user is removed from an org, this is a valid case.
logger.info("User is not in organization", {
userId: key.receiverId,
orgId: project.orgId,
projectId: project.id
});
logger.info(
{
userId: key.receiverId,
orgId: project.orgId,
projectId: project.id
},
"User is not in organization"
);
// eslint-disable-next-line no-continue
continue;
}
@ -551,10 +554,10 @@ export const projectQueueFactory = ({
.catch(() => [null]);
if (!project) {
logger.error("Failed to upgrade project, because no project was found", data);
logger.error(data, "Failed to upgrade project, because no project was found");
} else {
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed);
logger.error("Failed to upgrade project", err, {
logger.error(err, "Failed to upgrade project", {
extra: {
project,
jobData: data

View File

@ -149,6 +149,7 @@ export const projectServiceFactory = ({
actorOrgId,
actorAuthMethod,
workspaceName,
workspaceDescription,
slug: projectSlug,
kmsKeyId,
tx: trx,
@ -206,6 +207,7 @@ export const projectServiceFactory = ({
const project = await projectDAL.create(
{
name: workspaceName,
description: workspaceDescription,
orgId: organization.id,
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
kmsSecretManagerKeyId: kmsKeyId,
@ -496,6 +498,7 @@ export const projectServiceFactory = ({
const updatedProject = await projectDAL.updateById(project.id, {
name: update.name,
description: update.description,
autoCapitalization: update.autoCapitalization
});
return updatedProject;

View File

@ -29,6 +29,7 @@ export type TCreateProjectDTO = {
actorId: string;
actorOrgId?: string;
workspaceName: string;
workspaceDescription?: string;
slug?: string;
kmsKeyId?: string;
createDefaultEnvs?: boolean;
@ -69,6 +70,7 @@ export type TUpdateProjectDTO = {
filter: Filter;
update: {
name?: string;
description?: string;
autoCapitalization?: boolean;
};
} & Omit<TProjectPermission, "projectId">;

View File

@ -142,7 +142,7 @@ export const fnTriggerWebhook = async ({
!isDisabled && picomatch.isMatch(secretPath, hookSecretPath, { strictSlashes: false })
);
if (!toBeTriggeredHooks.length) return;
logger.info("Secret webhook job started", { environment, secretPath, projectId });
logger.info({ environment, secretPath, projectId }, "Secret webhook job started");
const project = await projectDAL.findById(projectId);
const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) =>
@ -195,5 +195,5 @@ export const fnTriggerWebhook = async ({
);
}
});
logger.info("Secret webhook job ended", { environment, secretPath, projectId });
logger.info({ environment, secretPath, projectId }, "Secret webhook job ended");
};

View File

@ -111,7 +111,7 @@ var exportCmd = &cobra.Command{
accessToken = token.Token
} else {
log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err)
}

View File

@ -41,7 +41,7 @@ var initCmd = &cobra.Command{
}
}
userCreds, err := util.GetCurrentLoggedInUserDetails()
userCreds, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "Unable to get your login details")
}

View File

@ -154,6 +154,8 @@ var loginCmd = &cobra.Command{
DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) {
presetDomain := config.INFISICAL_URL
clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains")
if err != nil {
util.HandleError(err)
@ -198,7 +200,7 @@ var loginCmd = &cobra.Command{
// standalone user auth
if loginMethod == "user" {
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
currentLoggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
// if the key can't be found or there is an error getting current credentials from key ring, allow them to override
if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) {
log.Debug().Err(err)
@ -216,11 +218,19 @@ var loginCmd = &cobra.Command{
return
}
}
usePresetDomain, err := usePresetDomain(presetDomain)
if err != nil {
util.HandleError(err)
}
//override domain
domainQuery := true
if config.INFISICAL_URL_MANUAL_OVERRIDE != "" &&
config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_EU_URL) &&
config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) {
config.INFISICAL_URL_MANUAL_OVERRIDE != fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL) &&
!usePresetDomain {
overrideDomain, err := DomainOverridePrompt()
if err != nil {
util.HandleError(err)
@ -228,7 +238,7 @@ var loginCmd = &cobra.Command{
//if not override set INFISICAL_URL to exported var
//set domainQuery to false
if !overrideDomain {
if !overrideDomain && !usePresetDomain {
domainQuery = false
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL_MANUAL_OVERRIDE)
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", strings.TrimSuffix(config.INFISICAL_URL, "/api"))
@ -237,7 +247,7 @@ var loginCmd = &cobra.Command{
}
//prompt user to select domain between Infisical cloud and self-hosting
if domainQuery {
if domainQuery && !usePresetDomain {
err = askForDomain()
if err != nil {
util.HandleError(err, "Unable to parse domain url")
@ -526,6 +536,45 @@ func DomainOverridePrompt() (bool, error) {
return selectedOption == OVERRIDE, err
}
func usePresetDomain(presetDomain string) (bool, error) {
infisicalConfig, err := util.GetConfigFile()
if err != nil {
return false, fmt.Errorf("askForDomain: unable to get config file because [err=%s]", err)
}
preconfiguredUrl := strings.TrimSuffix(presetDomain, "/api")
if preconfiguredUrl != "" && preconfiguredUrl != util.INFISICAL_DEFAULT_US_URL && preconfiguredUrl != util.INFISICAL_DEFAULT_EU_URL {
parsedDomain := strings.TrimSuffix(strings.Trim(preconfiguredUrl, "/"), "/api")
_, err := url.ParseRequestURI(parsedDomain)
if err != nil {
return false, errors.New(fmt.Sprintf("Invalid domain URL: '%s'", parsedDomain))
}
config.INFISICAL_URL = fmt.Sprintf("%s/api", parsedDomain)
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", parsedDomain)
if !slices.Contains(infisicalConfig.Domains, parsedDomain) {
infisicalConfig.Domains = append(infisicalConfig.Domains, parsedDomain)
err = util.WriteConfigFile(&infisicalConfig)
if err != nil {
return false, fmt.Errorf("askForDomain: unable to write domains to config file because [err=%s]", err)
}
}
whilte := color.New(color.FgGreen)
boldWhite := whilte.Add(color.Bold)
time.Sleep(time.Second * 1)
boldWhite.Printf("[INFO] Using domain '%s' from domain flag or INFISICAL_API_URL environment variable\n", parsedDomain)
return true, nil
}
return false, nil
}
func askForDomain() error {
// query user to choose between Infisical cloud or self-hosting

View File

@ -54,7 +54,7 @@ func init() {
util.CheckForUpdate()
}
loggedInDetails, err := util.GetCurrentLoggedInUserDetails()
loggedInDetails, err := util.GetCurrentLoggedInUserDetails(false)
if !silent && err == nil && loggedInDetails.IsUserLoggedIn && !loggedInDetails.LoginExpired {
token, err := util.GetInfisicalToken(cmd)

View File

@ -194,7 +194,7 @@ var secretsSetCmd = &cobra.Command{
projectId = workspaceFile.WorkspaceId
}
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "unable to authenticate [err=%v]")
}
@ -278,7 +278,7 @@ var secretsDeleteCmd = &cobra.Command{
util.RequireLogin()
util.RequireLocalWorkspaceFile()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "Unable to authenticate")
}

View File

@ -41,7 +41,7 @@ var tokensCreateCmd = &cobra.Command{
},
Run: func(cmd *cobra.Command, args []string) {
// get plain text workspace key
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "Unable to retrieve your logged in your details. Please login in then try again")

View File

@ -55,7 +55,7 @@ func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentia
return userCredentials, err
}
func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
func GetCurrentLoggedInUserDetails(setConfigVariables bool) (LoggedInUserDetails, error) {
if ConfigFileExists() {
configFile, err := GetConfigFile()
if err != nil {
@ -75,18 +75,20 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
}
}
if setConfigVariables {
config.INFISICAL_URL_MANUAL_OVERRIDE = config.INFISICAL_URL
//configFile.LoggedInUserDomain
//if not empty set as infisical url
if configFile.LoggedInUserDomain != "" {
config.INFISICAL_URL = AppendAPIEndpoint(configFile.LoggedInUserDomain)
}
}
// check to to see if the JWT is still valid
httpClient := resty.New().
SetAuthToken(userCreds.JTWToken).
SetHeader("Accept", "application/json")
config.INFISICAL_URL_MANUAL_OVERRIDE = config.INFISICAL_URL
//configFile.LoggedInUserDomain
//if not empty set as infisical url
if configFile.LoggedInUserDomain != "" {
config.INFISICAL_URL = AppendAPIEndpoint(configFile.LoggedInUserDomain)
}
isAuthenticated := api.CallIsAuthenticated(httpClient)
// TODO: add refresh token
// if !isAuthenticated {

View File

@ -20,7 +20,7 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder
log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details")
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true)
if err != nil {
return nil, err
}
@ -177,7 +177,7 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er
if params.InfisicalToken == "" {
RequireLogin()
RequireLocalWorkspaceFile()
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true)
if err != nil {
return models.SingleFolder{}, err
@ -224,7 +224,7 @@ func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder,
RequireLogin()
RequireLocalWorkspaceFile()
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true)
if err != nil {
return nil, err

View File

@ -246,7 +246,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true)
isConnected := ValidateInfisicalAPIConnection()
if isConnected {

View File

@ -3,7 +3,7 @@ title: 'Install'
description: "Infisical's CLI is one of the best way to manage environments and secrets. Install it here"
---
The Infisical CLI is powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables.
The Infisical CLI is a powerful command line tool that can be used to retrieve, modify, export and inject secrets into any process or application as environment variables.
You can use it across various environments, whether it's local development, CI/CD, staging, or production.
## Installation

View File

@ -69,7 +69,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -131,12 +131,12 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -66,7 +66,7 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -138,12 +138,12 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -98,7 +98,7 @@ Click on Add assignments. Search for the application name you created and select
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -151,12 +151,12 @@ Click on Add assignments. Search for the application name you created and select
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -39,7 +39,7 @@ The above configuration allows user creation and granting permissions.
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -116,12 +116,12 @@ The above configuration allows user creation and granting permissions.
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -34,7 +34,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -114,12 +114,12 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -31,7 +31,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -171,7 +171,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>

View File

@ -30,7 +30,7 @@ Create a project scopped API Key with the required permission in your Mongo Atla
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -101,12 +101,12 @@ Create a project scopped API Key with the required permission in your Mongo Atla
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -31,7 +31,7 @@ Create a user with the required permission in your MongoDB instance. This user w
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -103,12 +103,12 @@ Create a user with the required permission in your MongoDB instance. This user w
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -105,12 +105,12 @@ Create a user with the required permission in your SQL instance. This user will
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete the lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete the lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -102,12 +102,12 @@ Create a user with the required permission in your SQL instance. This user will
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -102,12 +102,12 @@ Create a user with the required permission in your SQL instance. This user will
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -105,12 +105,12 @@ Create a user with the required permission in your SQL instance. This user will
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete the lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete the lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -28,7 +28,7 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -103,12 +103,12 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

Some files were not shown because too many files have changed in this diff Show More