mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 06:29:04 +00:00
Merge branch 'main' into octopus-deploy-integration
This commit is contained in:
@@ -69,13 +69,21 @@ RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Required for pkcs11js
|
# Required for pkcs11js and ODBC
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
|
unixodbc \
|
||||||
|
unixodbc-dev \
|
||||||
|
freetds-dev \
|
||||||
|
freetds-bin \
|
||||||
|
tdsodbc \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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 ./
|
COPY backend/package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
|
|
||||||
@@ -91,13 +99,21 @@ ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Required for pkcs11js
|
# Required for pkcs11js and ODBC
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3 \
|
python3 \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
|
unixodbc \
|
||||||
|
unixodbc-dev \
|
||||||
|
freetds-dev \
|
||||||
|
freetds-bin \
|
||||||
|
tdsodbc \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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 ./
|
COPY backend/package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
|
|
||||||
@@ -108,13 +124,24 @@ RUN mkdir frontend-build
|
|||||||
# Production stage
|
# Production stage
|
||||||
FROM base AS production
|
FROM base AS production
|
||||||
|
|
||||||
# Install necessary packages
|
# Install necessary packages including ODBC
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
git \
|
git \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
unixodbc \
|
||||||
|
unixodbc-dev \
|
||||||
|
freetds-dev \
|
||||||
|
freetds-bin \
|
||||||
|
tdsodbc \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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
|
# Install Infisical CLI
|
||||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
|
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 \
|
&& apt-get update && apt-get install -y infisical=0.31.1 \
|
||||||
|
|||||||
@@ -72,8 +72,16 @@ RUN addgroup --system --gid 1001 nodejs \
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Required for pkcs11js
|
# Install all required dependencies for build
|
||||||
RUN apk add --no-cache python3 make g++
|
RUN apk --update add \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
unixodbc \
|
||||||
|
freetds \
|
||||||
|
unixodbc-dev \
|
||||||
|
libc-dev \
|
||||||
|
freetds-dev
|
||||||
|
|
||||||
COPY backend/package*.json ./
|
COPY backend/package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
@@ -88,8 +96,19 @@ FROM base AS backend-runner
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Required for pkcs11js
|
# Install all required dependencies for runtime
|
||||||
RUN apk add --no-cache python3 make g++
|
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 ./
|
COPY backend/package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
@@ -100,11 +119,32 @@ RUN mkdir frontend-build
|
|||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM base AS production
|
FROM base AS production
|
||||||
|
|
||||||
RUN apk add --upgrade --no-cache ca-certificates
|
RUN apk add --upgrade --no-cache ca-certificates
|
||||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
RUN apk add --no-cache bash curl && curl -1sLf \
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
||||||
&& apk add infisical=0.31.1 && apk add --no-cache git
|
&& 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 \
|
RUN addgroup --system --gid 1001 nodejs \
|
||||||
&& adduser --system --uid 1001 non-root-user
|
&& adduser --system --uid 1001 non-root-user
|
||||||
|
|
||||||
@@ -127,7 +167,6 @@ ARG CAPTCHA_SITE_KEY
|
|||||||
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
ENV NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY \
|
||||||
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
BAKED_NEXT_PUBLIC_CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||||
|
|
||||||
WORKDIR /
|
|
||||||
|
|
||||||
COPY --from=backend-runner /app /backend
|
COPY --from=backend-runner /app /backend
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ RUN apk --update add \
|
|||||||
make \
|
make \
|
||||||
g++
|
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 ./
|
COPY package*.json ./
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
|
|
||||||
@@ -28,6 +37,17 @@ RUN apk --update add \
|
|||||||
make \
|
make \
|
||||||
g++
|
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
|
RUN npm ci --only-production && npm cache clean --force
|
||||||
|
|
||||||
COPY --from=build /app .
|
COPY --from=build /app .
|
||||||
|
|||||||
+13
-1
@@ -7,7 +7,7 @@ ARG SOFTHSM2_VERSION=2.5.0
|
|||||||
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
|
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
|
||||||
SOFTHSM2_SOURCES=/tmp/softhsm2
|
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 \
|
RUN apk --update add \
|
||||||
alpine-sdk \
|
alpine-sdk \
|
||||||
autoconf \
|
autoconf \
|
||||||
@@ -19,7 +19,19 @@ RUN apk --update add \
|
|||||||
make \
|
make \
|
||||||
g++
|
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
|
# build and install SoftHSM2
|
||||||
|
|
||||||
RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES}
|
RUN git clone https://github.com/opendnssec/SoftHSMv2.git ${SOFTHSM2_SOURCES}
|
||||||
WORKDIR ${SOFTHSM2_SOURCES}
|
WORKDIR ${SOFTHSM2_SOURCES}
|
||||||
|
|
||||||
|
|||||||
Generated
+32
@@ -24,6 +24,7 @@
|
|||||||
"@fastify/multipart": "8.3.0",
|
"@fastify/multipart": "8.3.0",
|
||||||
"@fastify/passport": "^2.4.0",
|
"@fastify/passport": "^2.4.0",
|
||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
|
"@fastify/request-context": "^5.1.0",
|
||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
"@fastify/swagger": "^8.14.0",
|
"@fastify/swagger": "^8.14.0",
|
||||||
"@fastify/swagger-ui": "^2.1.0",
|
"@fastify/swagger-ui": "^2.1.0",
|
||||||
@@ -81,6 +82,7 @@
|
|||||||
"mysql2": "^3.9.8",
|
"mysql2": "^3.9.8",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "^3.3.4",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
|
"odbc": "^2.4.9",
|
||||||
"openid-client": "^5.6.5",
|
"openid-client": "^5.6.5",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"oracledb": "^6.4.0",
|
"oracledb": "^6.4.0",
|
||||||
@@ -5529,6 +5531,15 @@
|
|||||||
"toad-cache": "^3.3.0"
|
"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": {
|
"node_modules/@fastify/send": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz",
|
||||||
@@ -17878,6 +17889,27 @@
|
|||||||
"jsonwebtoken": "^9.0.2"
|
"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": {
|
"node_modules/oidc-token-hash": {
|
||||||
"version": "5.0.3",
|
"version": "5.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"@fastify/multipart": "8.3.0",
|
"@fastify/multipart": "8.3.0",
|
||||||
"@fastify/passport": "^2.4.0",
|
"@fastify/passport": "^2.4.0",
|
||||||
"@fastify/rate-limit": "^9.0.0",
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
|
"@fastify/request-context": "^5.1.0",
|
||||||
"@fastify/session": "^10.7.0",
|
"@fastify/session": "^10.7.0",
|
||||||
"@fastify/swagger": "^8.14.0",
|
"@fastify/swagger": "^8.14.0",
|
||||||
"@fastify/swagger-ui": "^2.1.0",
|
"@fastify/swagger-ui": "^2.1.0",
|
||||||
@@ -189,6 +190,7 @@
|
|||||||
"mysql2": "^3.9.8",
|
"mysql2": "^3.9.8",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "^3.3.4",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
|
"odbc": "^2.4.9",
|
||||||
"openid-client": "^5.6.5",
|
"openid-client": "^5.6.5",
|
||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"oracledb": "^6.4.0",
|
"oracledb": "^6.4.0",
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import "@fastify/request-context";
|
||||||
|
|
||||||
|
declare module "@fastify/request-context" {
|
||||||
|
interface RequestContextData {
|
||||||
|
requestId: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault } from "fastify";
|
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";
|
import { ZodTypeProvider } from "@app/server/plugins/fastify-zod";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -8,7 +8,7 @@ declare global {
|
|||||||
RawServerDefault,
|
RawServerDefault,
|
||||||
RawRequestDefaultExpression<RawServerDefault>,
|
RawRequestDefaultExpression<RawServerDefault>,
|
||||||
RawReplyDefaultExpression<RawServerDefault>,
|
RawReplyDefaultExpression<RawServerDefault>,
|
||||||
Readonly<Logger>,
|
Readonly<CustomLogger>,
|
||||||
ZodTypeProvider
|
ZodTypeProvider
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const KmsRootConfigSchema = z.object({
|
export const KmsRootConfigSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
encryptedRootKey: zodBuffer,
|
encryptedRootKey: zodBuffer,
|
||||||
encryptionStrategy: z.string(),
|
encryptionStrategy: z.string().default("SOFTWARE").nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export const ProjectsSchema = z.object({
|
|||||||
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
||||||
auditLogsRetentionDays: z.number().nullable().optional(),
|
auditLogsRetentionDays: z.number().nullable().optional(),
|
||||||
kmsSecretManagerKeyId: z.string().uuid().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>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
|||||||
@@ -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
|
// 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({
|
const addUserToGroupCommand = new ModifyUserGroupCommand({
|
||||||
@@ -96,7 +96,7 @@ const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: strin
|
|||||||
await ensureInfisicalGroupExists(clusterName);
|
await ensureInfisicalGroupExists(clusterName);
|
||||||
|
|
||||||
await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user
|
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 {
|
return {
|
||||||
userId: creationInput.UserId,
|
userId: creationInput.UserId,
|
||||||
@@ -212,7 +212,7 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
// Do nothing
|
// No renewal necessary
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretAwsIamSchema>) => {
|
||||||
const client = new IAMClient({
|
const client = new IAMClient({
|
||||||
region: providerInputs.region,
|
region: providerInputs.region,
|
||||||
credentials: {
|
credentials: {
|
||||||
@@ -47,7 +47,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const isConnected = await client.send(new GetUserCommand({})).then(() => true);
|
const isConnected = await client.send(new GetUserCommand({})).then(() => true);
|
||||||
return isConnected;
|
return isConnected;
|
||||||
@@ -55,7 +55,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown) => {
|
const create = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
||||||
@@ -118,7 +118,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
|
|
||||||
@@ -179,9 +179,8 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (_inputs: unknown, entityId: string) => {
|
const renew = async (_inputs: unknown, entityId: string) => {
|
||||||
// do nothing
|
// No renewal necessary
|
||||||
const username = entityId;
|
return { entityId };
|
||||||
return { entityId: username };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getToken = async (
|
const $getToken = async (
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
applicationId: string,
|
applicationId: string,
|
||||||
clientSecret: string
|
clientSecret: string
|
||||||
@@ -51,18 +51,13 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
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;
|
return data.success;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
|
||||||
// Do nothing
|
|
||||||
return { entityId };
|
|
||||||
};
|
|
||||||
|
|
||||||
const create = async (inputs: unknown) => {
|
const create = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
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) {
|
if (!data.success) {
|
||||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
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 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) {
|
if (!data.success) {
|
||||||
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
throw new BadRequestError({ message: "Failed to authorize to Microsoft Entra ID" });
|
||||||
}
|
}
|
||||||
@@ -127,6 +122,11 @@ export const AzureEntraIDProvider = (): TDynamicProviderFns & {
|
|||||||
return users;
|
return users;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
|
// No renewal necessary
|
||||||
|
return { entityId };
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
validateProviderInputs,
|
validateProviderInputs,
|
||||||
validateConnection,
|
validateConnection,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
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 sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||||
const client = new cassandra.Client({
|
const client = new cassandra.Client({
|
||||||
sslOptions,
|
sslOptions,
|
||||||
@@ -47,7 +47,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
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);
|
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
|
||||||
await client.shutdown();
|
await client.shutdown();
|
||||||
@@ -56,7 +56,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown, expireAt: number) => {
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@@ -82,7 +82,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
const { keyspace } = providerInputs;
|
const { keyspace } = providerInputs;
|
||||||
@@ -99,20 +99,24 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
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 expiration = new Date(expireAt).toISOString();
|
||||||
const { keyspace } = providerInputs;
|
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);
|
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||||
for (const query of queries) {
|
for await (const query of queries) {
|
||||||
// eslint-disable-next-line
|
|
||||||
await client.execute(query);
|
await client.execute(query);
|
||||||
}
|
}
|
||||||
await client.shutdown();
|
await client.shutdown();
|
||||||
return { entityId: username };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
|
||||||
const connection = new ElasticSearchClient({
|
const connection = new ElasticSearchClient({
|
||||||
node: {
|
node: {
|
||||||
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
|
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
|
||||||
@@ -55,7 +55,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const infoResponse = await connection
|
const infoResponse = await connection
|
||||||
.info()
|
.info()
|
||||||
@@ -67,7 +67,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown) => {
|
const create = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@@ -85,7 +85,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
await connection.security.deleteUser({
|
await connection.security.deleteUser({
|
||||||
username: entityId
|
username: entityId
|
||||||
@@ -96,7 +96,7 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
// Do nothing
|
// No renewal necessary
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,17 @@ import { AzureEntraIDProvider } from "./azure-entra-id";
|
|||||||
import { CassandraProvider } from "./cassandra";
|
import { CassandraProvider } from "./cassandra";
|
||||||
import { ElasticSearchProvider } from "./elastic-search";
|
import { ElasticSearchProvider } from "./elastic-search";
|
||||||
import { LdapProvider } from "./ldap";
|
import { LdapProvider } from "./ldap";
|
||||||
import { DynamicSecretProviders } from "./models";
|
import { DynamicSecretProviders, TDynamicProviderFns } from "./models";
|
||||||
import { MongoAtlasProvider } from "./mongo-atlas";
|
import { MongoAtlasProvider } from "./mongo-atlas";
|
||||||
import { MongoDBProvider } from "./mongo-db";
|
import { MongoDBProvider } from "./mongo-db";
|
||||||
import { RabbitMqProvider } from "./rabbit-mq";
|
import { RabbitMqProvider } from "./rabbit-mq";
|
||||||
import { RedisDatabaseProvider } from "./redis";
|
import { RedisDatabaseProvider } from "./redis";
|
||||||
|
import { SapAseProvider } from "./sap-ase";
|
||||||
import { SapHanaProvider } from "./sap-hana";
|
import { SapHanaProvider } from "./sap-hana";
|
||||||
import { SqlDatabaseProvider } from "./sql-database";
|
import { SqlDatabaseProvider } from "./sql-database";
|
||||||
import { TotpProvider } from "./totp";
|
import { TotpProvider } from "./totp";
|
||||||
|
|
||||||
export const buildDynamicSecretProviders = () => ({
|
export const buildDynamicSecretProviders = (): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||||
@@ -29,5 +30,6 @@ export const buildDynamicSecretProviders = () => ({
|
|||||||
[DynamicSecretProviders.Ldap]: LdapProvider(),
|
[DynamicSecretProviders.Ldap]: LdapProvider(),
|
||||||
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
|
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
|
||||||
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
|
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
|
||||||
[DynamicSecretProviders.Totp]: TotpProvider()
|
[DynamicSecretProviders.Totp]: TotpProvider(),
|
||||||
|
[DynamicSecretProviders.SapAse]: SapAseProvider()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const client = ldapjs.createClient({
|
const client = ldapjs.createClient({
|
||||||
url: providerInputs.url,
|
url: providerInputs.url,
|
||||||
@@ -83,7 +83,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
return client.connected;
|
return client.connected;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown) => {
|
const create = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
if (providerInputs.credentialType === LdapCredentialType.Static) {
|
if (providerInputs.credentialType === LdapCredentialType.Static) {
|
||||||
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
|
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
|
||||||
@@ -235,7 +235,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
if (providerInputs.credentialType === LdapCredentialType.Static) {
|
if (providerInputs.credentialType === LdapCredentialType.Static) {
|
||||||
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
|
const dnMatch = providerInputs.rotationLdif.match(/^dn:\s*(.+)/m);
|
||||||
@@ -268,7 +268,7 @@ export const LdapProvider = (): TDynamicProviderFns => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
// Do nothing
|
// No renewal necessary
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ export enum SqlProviders {
|
|||||||
Postgres = "postgres",
|
Postgres = "postgres",
|
||||||
MySQL = "mysql2",
|
MySQL = "mysql2",
|
||||||
Oracle = "oracledb",
|
Oracle = "oracledb",
|
||||||
MsSQL = "mssql"
|
MsSQL = "mssql",
|
||||||
|
SapAse = "sap-ase"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ElasticSearchAuthTypes {
|
export enum ElasticSearchAuthTypes {
|
||||||
@@ -118,6 +119,16 @@ export const DynamicSecretCassandraSchema = z.object({
|
|||||||
ca: z.string().optional()
|
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({
|
export const DynamicSecretAwsIamSchema = z.object({
|
||||||
accessKey: z.string().trim().min(1),
|
accessKey: z.string().trim().min(1),
|
||||||
secretAccessKey: z.string().trim().min(1),
|
secretAccessKey: z.string().trim().min(1),
|
||||||
@@ -274,12 +285,14 @@ export enum DynamicSecretProviders {
|
|||||||
Ldap = "ldap",
|
Ldap = "ldap",
|
||||||
SapHana = "sap-hana",
|
SapHana = "sap-hana",
|
||||||
Snowflake = "snowflake",
|
Snowflake = "snowflake",
|
||||||
Totp = "totp"
|
Totp = "totp",
|
||||||
|
SapAse = "sap-ase"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
|
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.AwsIam), inputs: DynamicSecretAwsIamSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
|
||||||
z.object({ type: z.literal(DynamicSecretProviders.SapHana), inputs: DynamicSecretSapHanaSchema }),
|
z.object({ type: z.literal(DynamicSecretProviders.SapHana), inputs: DynamicSecretSapHanaSchema }),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoAtlasSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoAtlasSchema>) => {
|
||||||
const client = axios.create({
|
const client = axios.create({
|
||||||
baseURL: "https://cloud.mongodb.com/api/atlas",
|
baseURL: "https://cloud.mongodb.com/api/atlas",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -40,7 +40,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const isConnected = await client({
|
const isConnected = await client({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -59,7 +59,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown, expireAt: number) => {
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@@ -87,7 +87,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
const isExisting = await client({
|
const isExisting = await client({
|
||||||
@@ -114,7 +114,7 @@ export const MongoAtlasProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
|
||||||
const isSrv = !providerInputs.port;
|
const isSrv = !providerInputs.port;
|
||||||
const uri = isSrv
|
const uri = isSrv
|
||||||
? `mongodb+srv://${providerInputs.host}`
|
? `mongodb+srv://${providerInputs.host}`
|
||||||
@@ -42,7 +42,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const isConnected = await client
|
const isConnected = await client
|
||||||
.db(providerInputs.database)
|
.db(providerInputs.database)
|
||||||
@@ -55,7 +55,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown) => {
|
const create = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@@ -74,7 +74,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
|
|
||||||
@@ -88,6 +88,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (_inputs: unknown, entityId: string) => {
|
const renew = async (_inputs: unknown, entityId: string) => {
|
||||||
|
// No renewal necessary
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`,
|
baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`,
|
||||||
auth: {
|
auth: {
|
||||||
@@ -105,7 +105,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const infoResponse = await connection.get("/whoami").then(() => true);
|
const infoResponse = await connection.get("/whoami").then(() => true);
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown) => {
|
const create = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@@ -134,7 +134,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
|
await deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string) => {
|
const renew = async (inputs: unknown, entityId: string) => {
|
||||||
// Do nothing
|
// No renewal necessary
|
||||||
return { entityId };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
|
||||||
let connection: Redis | null = null;
|
let connection: Redis | null = null;
|
||||||
try {
|
try {
|
||||||
connection = new Redis({
|
connection = new Redis({
|
||||||
@@ -92,7 +92,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const pingResponse = await connection
|
const pingResponse = await connection
|
||||||
.ping()
|
.ping()
|
||||||
@@ -104,7 +104,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const create = async (inputs: unknown, expireAt: number) => {
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@@ -126,7 +126,7 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
|
|
||||||
@@ -141,7 +141,9 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const connection = await getClient(providerInputs);
|
if (!providerInputs.renewStatement) return { entityId };
|
||||||
|
|
||||||
|
const connection = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -32,7 +32,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
|
||||||
const client = hdb.createClient({
|
const client = hdb.createClient({
|
||||||
host: providerInputs.host,
|
host: providerInputs.host,
|
||||||
port: providerInputs.port,
|
port: providerInputs.port,
|
||||||
@@ -64,9 +64,9 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
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) => {
|
client.exec("SELECT 1 FROM DUMMY;", (err: any) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject();
|
reject();
|
||||||
@@ -86,7 +86,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
const expiration = new Date(expireAt).toISOString();
|
const expiration = new Date(expireAt).toISOString();
|
||||||
|
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
@@ -114,7 +114,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, username: string) => {
|
const revoke = async (inputs: unknown, username: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||||
for await (const query of queries) {
|
for await (const query of queries) {
|
||||||
@@ -135,13 +135,15 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
return { entityId: username };
|
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 providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
if (!providerInputs.renewStatement) return { entityId };
|
||||||
|
|
||||||
|
const client = await $getClient(providerInputs);
|
||||||
try {
|
try {
|
||||||
const expiration = new Date(expireAt).toISOString();
|
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);
|
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||||
for await (const query of queries) {
|
for await (const query of queries) {
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
@@ -161,7 +163,7 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
|||||||
client.disconnect();
|
client.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { entityId: username };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
return providerInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretSnowflakeSchema>) => {
|
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSnowflakeSchema>) => {
|
||||||
const client = snowflake.createConnection({
|
const client = snowflake.createConnection({
|
||||||
account: `${providerInputs.orgId}-${providerInputs.accountId}`,
|
account: `${providerInputs.orgId}-${providerInputs.accountId}`,
|
||||||
username: providerInputs.username,
|
username: providerInputs.username,
|
||||||
@@ -49,7 +49,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
let isValidConnection: boolean;
|
let isValidConnection: boolean;
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
|||||||
const create = async (inputs: unknown, expireAt: number) => {
|
const create = async (inputs: unknown, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
const password = generatePassword();
|
||||||
@@ -107,7 +107,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
|||||||
const revoke = async (inputs: unknown, username: string) => {
|
const revoke = async (inputs: unknown, username: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
const client = await getClient(providerInputs);
|
const client = await $getClient(providerInputs);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
|
||||||
@@ -131,17 +131,16 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
|||||||
return { entityId: username };
|
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 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 {
|
try {
|
||||||
const expiration = getDaysToExpiry(new Date(expireAt));
|
const expiration = getDaysToExpiry(new Date(expireAt));
|
||||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||||
username,
|
username: entityId,
|
||||||
expiration
|
expiration
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -161,7 +160,7 @@ export const SnowflakeProvider = (): TDynamicProviderFns => {
|
|||||||
client.destroy(noop);
|
client.destroy(noop);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { entityId: username };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
return providerInputs;
|
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 ssl = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||||
const db = knex({
|
const db = knex({
|
||||||
client: providerInputs.client,
|
client: providerInputs.client,
|
||||||
@@ -52,7 +52,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const validateConnection = async (inputs: unknown) => {
|
const validateConnection = async (inputs: unknown) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const db = await getClient(providerInputs);
|
const db = await $getClient(providerInputs);
|
||||||
// oracle needs from keyword
|
// oracle needs from keyword
|
||||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
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 create = async (inputs: unknown, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const db = await getClient(providerInputs);
|
const db = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = generateUsername(providerInputs.client);
|
const username = generateUsername(providerInputs.client);
|
||||||
const password = generatePassword(providerInputs.client);
|
const password = generatePassword(providerInputs.client);
|
||||||
@@ -90,7 +90,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (inputs: unknown, entityId: string) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
const db = await getClient(providerInputs);
|
const db = await $getClient(providerInputs);
|
||||||
|
|
||||||
const username = entityId;
|
const username = entityId;
|
||||||
const { database } = providerInputs;
|
const { database } = providerInputs;
|
||||||
@@ -110,13 +110,19 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
|
|
||||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
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 expiration = new Date(expireAt).toISOString();
|
||||||
const { database } = providerInputs;
|
const { database } = providerInputs;
|
||||||
|
|
||||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database });
|
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||||
|
username: entityId,
|
||||||
|
expiration,
|
||||||
|
database
|
||||||
|
});
|
||||||
|
|
||||||
if (renewStatement) {
|
if (renewStatement) {
|
||||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
@@ -128,7 +134,7 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
return { entityId: username };
|
return { entityId };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { authenticator } from "otplib";
|
import { authenticator } from "otplib";
|
||||||
import { HashAlgorithms } from "otplib/core";
|
import { HashAlgorithms } from "otplib/core";
|
||||||
|
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models";
|
import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models";
|
||||||
@@ -76,10 +75,9 @@ export const TotpProvider = (): TDynamicProviderFns => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const renew = async (_inputs: unknown, _entityId: string) => {
|
const renew = async (_inputs: unknown, entityId: string) => {
|
||||||
throw new BadRequestError({
|
// No renewal necessary
|
||||||
message: "Lease renewal is not supported for TOTPs"
|
return { entityId };
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const initializeHsmModule = () => {
|
|||||||
|
|
||||||
logger.info("PKCS#11 module initialized");
|
logger.info("PKCS#11 module initialized");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Failed to initialize PKCS#11 module:", err);
|
logger.error(err, "Failed to initialize PKCS#11 module");
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -39,7 +39,7 @@ export const initializeHsmModule = () => {
|
|||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
logger.info("PKCS#11 module finalized");
|
logger.info("PKCS#11 module finalized");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Failed to finalize PKCS#11 module:", err);
|
logger.error(err, "Failed to finalize PKCS#11 module");
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise<boolean>
|
|||||||
});
|
});
|
||||||
|
|
||||||
ldapClient.on("error", (err) => {
|
ldapClient.on("error", (err) => {
|
||||||
logger.error("LDAP client error:", err);
|
logger.error(err, "LDAP client error");
|
||||||
logger.error(err);
|
|
||||||
resolve(false);
|
resolve(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ export const licenseServiceFactory = ({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.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(
|
await keyStore.setItemWithExpiry(
|
||||||
FEATURE_CACHE_KEY(orgId),
|
FEATURE_CACHE_KEY(orgId),
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
|
|||||||
}
|
}
|
||||||
return rateLimit;
|
return rateLimit;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Error fetching rate limits %o", err);
|
logger.error(err, "Error fetching rate limits");
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -69,12 +69,12 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
|
|||||||
mfaRateLimit: rateLimit.mfaRateLimit
|
mfaRateLimit: rateLimit.mfaRateLimit
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);
|
logger.info(newRateLimitMaxConfiguration, "syncRateLimitConfiguration: rate limit configuration");
|
||||||
Object.freeze(newRateLimitMaxConfiguration);
|
Object.freeze(newRateLimitMaxConfiguration);
|
||||||
rateLimitMaxConfiguration = newRateLimitMaxConfiguration;
|
rateLimitMaxConfiguration = newRateLimitMaxConfiguration;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error syncing rate limit configurations: %o`, error);
|
logger.error(error, "Error syncing rate limit configurations");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -238,11 +238,11 @@ export const secretScanningQueueFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
queueService.listen(QueueName.SecretPushEventScan, "failed", (job, err) => {
|
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) => {
|
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 };
|
return { startFullRepoScan, startPushEventScan };
|
||||||
|
|||||||
@@ -391,6 +391,7 @@ export const PROJECTS = {
|
|||||||
CREATE: {
|
CREATE: {
|
||||||
organizationSlug: "The slug of the organization to create the project in.",
|
organizationSlug: "The slug of the organization to create the project in.",
|
||||||
projectName: "The name of the project to create.",
|
projectName: "The name of the project to create.",
|
||||||
|
projectDescription: "An optional description label for the project.",
|
||||||
slug: "An optional slug for the project.",
|
slug: "An optional slug for the project.",
|
||||||
template: "The name of the project template, if specified, to apply to this project."
|
template: "The name of the project template, if specified, to apply to this project."
|
||||||
},
|
},
|
||||||
@@ -403,6 +404,7 @@ export const PROJECTS = {
|
|||||||
UPDATE: {
|
UPDATE: {
|
||||||
workspaceId: "The ID of the project to update.",
|
workspaceId: "The ID of the project to update.",
|
||||||
name: "The new name of the project.",
|
name: "The new name of the project.",
|
||||||
|
projectDescription: "An optional description label for the project.",
|
||||||
autoCapitalization: "Disable or enable auto-capitalization for the project."
|
autoCapitalization: "Disable or enable auto-capitalization for the project."
|
||||||
},
|
},
|
||||||
GET_KEY: {
|
GET_KEY: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Logger } from "pino";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { removeTrailingSlash } from "../fn";
|
import { removeTrailingSlash } from "../fn";
|
||||||
|
import { CustomLogger } from "../logger/logger";
|
||||||
import { zpStr } from "../zod";
|
import { zpStr } from "../zod";
|
||||||
|
|
||||||
export const GITLAB_URL = "https://gitlab.com";
|
export const GITLAB_URL = "https://gitlab.com";
|
||||||
@@ -212,7 +212,7 @@ let envCfg: Readonly<z.infer<typeof envSchema>>;
|
|||||||
|
|
||||||
export const getConfig = () => envCfg;
|
export const getConfig = () => envCfg;
|
||||||
// cannot import singleton logger directly as it needs config to load various transport
|
// 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);
|
const parsedEnv = envSchema.safeParse(process.env);
|
||||||
if (!parsedEnv.success) {
|
if (!parsedEnv.success) {
|
||||||
(logger ?? console).error("Invalid environment variables. Check the error below");
|
(logger ?? console).error("Invalid environment variables. Check the error below");
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
// logger follows a singleton pattern
|
// logger follows a singleton pattern
|
||||||
// easier to use it that's all.
|
// easier to use it that's all.
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import pino, { Logger } from "pino";
|
import pino, { Logger } from "pino";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -13,14 +15,37 @@ const logLevelToSeverityLookup: Record<string, string> = {
|
|||||||
"60": "CRITICAL"
|
"60": "CRITICAL"
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-mutable-exports
|
|
||||||
export let logger: Readonly<Logger>;
|
|
||||||
// akhilmhdh:
|
// akhilmhdh:
|
||||||
// The logger is not placed in the main app config to avoid a circular dependency.
|
// 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.
|
// 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.
|
// 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.
|
// 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({
|
const loggerConfig = z.object({
|
||||||
AWS_CLOUDWATCH_LOG_GROUP_NAME: z.string().default("infisical-log-stream"),
|
AWS_CLOUDWATCH_LOG_GROUP_NAME: z.string().default("infisical-log-stream"),
|
||||||
AWS_CLOUDWATCH_LOG_REGION: z.string().default("us-east-1"),
|
AWS_CLOUDWATCH_LOG_REGION: z.string().default("us-east-1"),
|
||||||
@@ -62,6 +87,17 @@ const redactedKeys = [
|
|||||||
"config"
|
"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 () => {
|
export const initLogger = async () => {
|
||||||
const cfg = loggerConfig.parse(process.env);
|
const cfg = loggerConfig.parse(process.env);
|
||||||
const targets: pino.TransportMultiOptions["targets"][number][] = [
|
const targets: pino.TransportMultiOptions["targets"][number][] = [
|
||||||
@@ -94,6 +130,30 @@ export const initLogger = async () => {
|
|||||||
targets
|
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(
|
logger = pino(
|
||||||
{
|
{
|
||||||
mixin(_context, level) {
|
mixin(_context, level) {
|
||||||
@@ -113,5 +173,6 @@ export const initLogger = async () => {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
transport
|
transport
|
||||||
);
|
);
|
||||||
return logger;
|
|
||||||
|
return wrapLogger(logger);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import fastifyFormBody from "@fastify/formbody";
|
|||||||
import helmet from "@fastify/helmet";
|
import helmet from "@fastify/helmet";
|
||||||
import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
|
import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
|
||||||
import ratelimiter from "@fastify/rate-limit";
|
import ratelimiter from "@fastify/rate-limit";
|
||||||
|
import { fastifyRequestContext } from "@fastify/request-context";
|
||||||
import fastify from "fastify";
|
import fastify from "fastify";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
import { Logger } from "pino";
|
|
||||||
|
|
||||||
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
|
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig, IS_PACKAGED } from "@app/lib/config/env";
|
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 { TQueueServiceFactory } from "@app/queue";
|
||||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
|
||||||
@@ -35,7 +37,7 @@ type TMain = {
|
|||||||
auditLogDb?: Knex;
|
auditLogDb?: Knex;
|
||||||
db: Knex;
|
db: Knex;
|
||||||
smtp: TSmtpService;
|
smtp: TSmtpService;
|
||||||
logger?: Logger;
|
logger?: CustomLogger;
|
||||||
queue: TQueueServiceFactory;
|
queue: TQueueServiceFactory;
|
||||||
keyStore: TKeyStoreFactory;
|
keyStore: TKeyStoreFactory;
|
||||||
hsmModule: HsmModule;
|
hsmModule: HsmModule;
|
||||||
@@ -47,7 +49,9 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
|
|||||||
|
|
||||||
const server = fastify({
|
const server = fastify({
|
||||||
logger: appCfg.NODE_ENV === "test" ? false : logger,
|
logger: appCfg.NODE_ENV === "test" ? false : logger,
|
||||||
|
genReqId: () => `req-${alphaNumericNanoId(14)}`,
|
||||||
trustProxy: true,
|
trustProxy: true,
|
||||||
|
|
||||||
connectionTimeout: appCfg.isHsmConfigured ? 90_000 : 30_000,
|
connectionTimeout: appCfg.isHsmConfigured ? 90_000 : 30_000,
|
||||||
ignoreTrailingSlash: true,
|
ignoreTrailingSlash: true,
|
||||||
pluginTimeout: 40_000
|
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(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 });
|
await server.register(registerRoutes, { smtp, queue, db, auditLogDb, keyStore, hsmModule });
|
||||||
|
|
||||||
if (appCfg.isProductionMode) {
|
if (appCfg.isProductionMode) {
|
||||||
|
|||||||
@@ -39,29 +39,42 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError) {
|
||||||
void res
|
void res
|
||||||
.status(HttpStatusCodes.BadRequest)
|
.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) {
|
} else if (error instanceof NotFoundError) {
|
||||||
void res
|
void res
|
||||||
.status(HttpStatusCodes.NotFound)
|
.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) {
|
} else if (error instanceof UnauthorizedError) {
|
||||||
void res
|
void res.status(HttpStatusCodes.Unauthorized).send({
|
||||||
.status(HttpStatusCodes.Unauthorized)
|
requestId: req.id,
|
||||||
.send({ statusCode: HttpStatusCodes.Unauthorized, message: error.message, error: error.name });
|
statusCode: HttpStatusCodes.Unauthorized,
|
||||||
|
message: error.message,
|
||||||
|
error: error.name
|
||||||
|
});
|
||||||
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
|
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {
|
||||||
void res
|
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||||
.status(HttpStatusCodes.InternalServerError)
|
requestId: req.id,
|
||||||
.send({ statusCode: HttpStatusCodes.InternalServerError, message: "Something went wrong", error: error.name });
|
statusCode: HttpStatusCodes.InternalServerError,
|
||||||
|
message: "Something went wrong",
|
||||||
|
error: error.name
|
||||||
|
});
|
||||||
} else if (error instanceof GatewayTimeoutError) {
|
} else if (error instanceof GatewayTimeoutError) {
|
||||||
void res
|
void res.status(HttpStatusCodes.GatewayTimeout).send({
|
||||||
.status(HttpStatusCodes.GatewayTimeout)
|
requestId: req.id,
|
||||||
.send({ statusCode: HttpStatusCodes.GatewayTimeout, message: error.message, error: error.name });
|
statusCode: HttpStatusCodes.GatewayTimeout,
|
||||||
|
message: error.message,
|
||||||
|
error: error.name
|
||||||
|
});
|
||||||
} else if (error instanceof ZodError) {
|
} else if (error instanceof ZodError) {
|
||||||
void res
|
void res.status(HttpStatusCodes.Unauthorized).send({
|
||||||
.status(HttpStatusCodes.Unauthorized)
|
requestId: req.id,
|
||||||
.send({ statusCode: HttpStatusCodes.Unauthorized, error: "ValidationFailure", message: error.issues });
|
statusCode: HttpStatusCodes.Unauthorized,
|
||||||
|
error: "ValidationFailure",
|
||||||
|
message: error.issues
|
||||||
|
});
|
||||||
} else if (error instanceof ForbiddenError) {
|
} else if (error instanceof ForbiddenError) {
|
||||||
void res.status(HttpStatusCodes.Forbidden).send({
|
void res.status(HttpStatusCodes.Forbidden).send({
|
||||||
|
requestId: req.id,
|
||||||
statusCode: HttpStatusCodes.Forbidden,
|
statusCode: HttpStatusCodes.Forbidden,
|
||||||
error: "PermissionDenied",
|
error: "PermissionDenied",
|
||||||
message: `You are not allowed to ${error.action} on ${error.subjectType}`,
|
message: `You are not allowed to ${error.action} on ${error.subjectType}`,
|
||||||
@@ -74,48 +87,54 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
});
|
});
|
||||||
} else if (error instanceof ForbiddenRequestError) {
|
} else if (error instanceof ForbiddenRequestError) {
|
||||||
void res.status(HttpStatusCodes.Forbidden).send({
|
void res.status(HttpStatusCodes.Forbidden).send({
|
||||||
|
requestId: req.id,
|
||||||
statusCode: HttpStatusCodes.Forbidden,
|
statusCode: HttpStatusCodes.Forbidden,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
error: error.name
|
error: error.name
|
||||||
});
|
});
|
||||||
} else if (error instanceof RateLimitError) {
|
} else if (error instanceof RateLimitError) {
|
||||||
void res.status(HttpStatusCodes.TooManyRequests).send({
|
void res.status(HttpStatusCodes.TooManyRequests).send({
|
||||||
|
requestId: req.id,
|
||||||
statusCode: HttpStatusCodes.TooManyRequests,
|
statusCode: HttpStatusCodes.TooManyRequests,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
error: error.name
|
error: error.name
|
||||||
});
|
});
|
||||||
} else if (error instanceof ScimRequestError) {
|
} else if (error instanceof ScimRequestError) {
|
||||||
void res.status(error.status).send({
|
void res.status(error.status).send({
|
||||||
|
requestId: req.id,
|
||||||
schemas: error.schemas,
|
schemas: error.schemas,
|
||||||
status: error.status,
|
status: error.status,
|
||||||
detail: error.detail
|
detail: error.detail
|
||||||
});
|
});
|
||||||
} else if (error instanceof OidcAuthError) {
|
} else if (error instanceof OidcAuthError) {
|
||||||
void res
|
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||||
.status(HttpStatusCodes.InternalServerError)
|
requestId: req.id,
|
||||||
.send({ statusCode: HttpStatusCodes.InternalServerError, message: error.message, error: error.name });
|
statusCode: HttpStatusCodes.InternalServerError,
|
||||||
|
message: error.message,
|
||||||
|
error: error.name
|
||||||
|
});
|
||||||
} else if (error instanceof jwt.JsonWebTokenError) {
|
} else if (error instanceof jwt.JsonWebTokenError) {
|
||||||
const message = (() => {
|
let errorMessage = error.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.";
|
|
||||||
}
|
|
||||||
|
|
||||||
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({
|
void res.status(HttpStatusCodes.Forbidden).send({
|
||||||
|
requestId: req.id,
|
||||||
statusCode: HttpStatusCodes.Forbidden,
|
statusCode: HttpStatusCodes.Forbidden,
|
||||||
error: "TokenError",
|
error: "TokenError",
|
||||||
message
|
message: errorMessage
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
void res.status(HttpStatusCodes.InternalServerError).send({
|
void res.status(HttpStatusCodes.InternalServerError).send({
|
||||||
|
requestId: req.id,
|
||||||
statusCode: HttpStatusCodes.InternalServerError,
|
statusCode: HttpStatusCodes.InternalServerError,
|
||||||
error: "InternalServerError",
|
error: "InternalServerError",
|
||||||
message: "Something went wrong"
|
message: "Something went wrong"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const registerSecretScannerGhApp = async (server: FastifyZodProvider) =>
|
|||||||
|
|
||||||
app.on("installation", async (context) => {
|
app.on("installation", async (context) => {
|
||||||
const { payload } = 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) => {
|
app.on("push", async (context) => {
|
||||||
|
|||||||
@@ -30,27 +30,32 @@ export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
|
|||||||
|
|
||||||
export const DefaultResponseErrorsSchema = {
|
export const DefaultResponseErrorsSchema = {
|
||||||
400: z.object({
|
400: z.object({
|
||||||
|
requestId: z.string(),
|
||||||
statusCode: z.literal(400),
|
statusCode: z.literal(400),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
error: z.string()
|
error: z.string()
|
||||||
}),
|
}),
|
||||||
404: z.object({
|
404: z.object({
|
||||||
|
requestId: z.string(),
|
||||||
statusCode: z.literal(404),
|
statusCode: z.literal(404),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
error: z.string()
|
error: z.string()
|
||||||
}),
|
}),
|
||||||
401: z.object({
|
401: z.object({
|
||||||
|
requestId: z.string(),
|
||||||
statusCode: z.literal(401),
|
statusCode: z.literal(401),
|
||||||
message: z.any(),
|
message: z.any(),
|
||||||
error: z.string()
|
error: z.string()
|
||||||
}),
|
}),
|
||||||
403: z.object({
|
403: z.object({
|
||||||
|
requestId: z.string(),
|
||||||
statusCode: z.literal(403),
|
statusCode: z.literal(403),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
details: z.any().optional(),
|
details: z.any().optional(),
|
||||||
error: z.string()
|
error: z.string()
|
||||||
}),
|
}),
|
||||||
500: z.object({
|
500: z.object({
|
||||||
|
requestId: z.string(),
|
||||||
statusCode: z.literal(500),
|
statusCode: z.literal(500),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
error: z.string()
|
error: z.string()
|
||||||
@@ -207,6 +212,7 @@ export const SanitizedAuditLogStreamSchema = z.object({
|
|||||||
export const SanitizedProjectSchema = ProjectsSchema.pick({
|
export const SanitizedProjectSchema = ProjectsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
description: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
autoCapitalization: true,
|
autoCapitalization: true,
|
||||||
orgId: true,
|
orgId: true,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema";
|
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 { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
import {} from "../sanitizedSchemas";
|
import {} from "../sanitizedSchemas";
|
||||||
@@ -206,6 +207,33 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
|||||||
id: req.params.integrationId
|
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 };
|
return { integration };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -296,6 +296,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
.max(64, { message: "Name must be 64 or fewer characters" })
|
.max(64, { message: "Name must be 64 or fewer characters" })
|
||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECTS.UPDATE.name),
|
.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)
|
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -313,6 +319,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
|
description: req.body.description,
|
||||||
autoCapitalization: req.body.autoCapitalization
|
autoCapitalization: req.body.autoCapitalization
|
||||||
},
|
},
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
],
|
],
|
||||||
body: z.object({
|
body: z.object({
|
||||||
projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
|
projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
|
||||||
|
projectDescription: z.string().trim().optional().describe(PROJECTS.CREATE.projectDescription),
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
.min(5)
|
.min(5)
|
||||||
@@ -194,6 +195,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
workspaceName: req.body.projectName,
|
workspaceName: req.body.projectName,
|
||||||
|
workspaceDescription: req.body.projectDescription,
|
||||||
slug: req.body.slug,
|
slug: req.body.slug,
|
||||||
kmsKeyId: req.body.kmsKeyId,
|
kmsKeyId: req.body.kmsKeyId,
|
||||||
template: req.body.template
|
template: req.body.template
|
||||||
@@ -312,8 +314,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
slug: slugSchema.describe("The slug of the project to update.")
|
slug: slugSchema.describe("The slug of the project to update.")
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().optional().describe("The new name of the project."),
|
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
|
||||||
autoCapitalization: z.boolean().optional().describe("The new auto-capitalization setting.")
|
description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription),
|
||||||
|
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: SanitizedProjectSchema
|
200: SanitizedProjectSchema
|
||||||
@@ -330,6 +333,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
|
description: req.body.description,
|
||||||
autoCapitalization: req.body.autoCapitalization
|
autoCapitalization: req.body.autoCapitalization
|
||||||
},
|
},
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
|||||||
@@ -119,13 +119,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
if (!userAgent) throw new Error("user agent header is required");
|
if (!userAgent) throw new Error("user agent header is required");
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const serverCfg = await getServerCfg();
|
|
||||||
if (!serverCfg.allowSignUp) {
|
|
||||||
throw new ForbiddenRequestError({
|
|
||||||
message: "Signup's are disabled"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { user, accessToken, refreshToken, organizationId } =
|
const { user, accessToken, refreshToken, organizationId } =
|
||||||
await server.services.signup.completeEmailAccountSignup({
|
await server.services.signup.completeEmailAccountSignup({
|
||||||
...req.body,
|
...req.body,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
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 { isDisposableEmail } from "@app/lib/validator";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/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 { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
|
import { getServerCfg } from "../super-admin/super-admin-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { UserEncryption } from "../user/user-types";
|
import { UserEncryption } from "../user/user-types";
|
||||||
import { TAuthDALFactory } from "./auth-dal";
|
import { TAuthDALFactory } from "./auth-dal";
|
||||||
@@ -151,6 +152,8 @@ export const authSignupServiceFactory = ({
|
|||||||
authorization
|
authorization
|
||||||
}: TCompleteAccountSignupDTO) => {
|
}: TCompleteAccountSignupDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
const serverCfg = await getServerCfg();
|
||||||
|
|
||||||
const user = await userDAL.findOne({ username: email });
|
const user = await userDAL.findOne({ username: email });
|
||||||
if (!user || (user && user.isAccepted)) {
|
if (!user || (user && user.isAccepted)) {
|
||||||
throw new Error("Failed to complete account for complete user");
|
throw new Error("Failed to complete account for complete user");
|
||||||
@@ -163,6 +166,12 @@ export const authSignupServiceFactory = ({
|
|||||||
authMethod = userAuthMethod;
|
authMethod = userAuthMethod;
|
||||||
organizationId = orgId;
|
organizationId = orgId;
|
||||||
} else {
|
} 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);
|
validateSignUpAuthorization(authorization, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -182,7 +182,12 @@ export const identityProjectServiceFactory = ({
|
|||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
const customInputRoles = roles.filter(
|
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 hasCustomRole = Boolean(customInputRoles.length);
|
||||||
const customRoles = hasCustomRole
|
const customRoles = hasCustomRole
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth
|
|||||||
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
|
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
|
||||||
import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret";
|
import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { TSecretDALFactory } from "../secret/secret-dal";
|
import { TSecretDALFactory } from "../secret/secret-dal";
|
||||||
import { TSecretQueueFactory } from "../secret/secret-queue";
|
import { TSecretQueueFactory } from "../secret/secret-queue";
|
||||||
@@ -237,6 +238,46 @@ export const integrationServiceFactory = ({
|
|||||||
return { ...integration, envId: integration.environment.id };
|
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 ({
|
const deleteIntegration = async ({
|
||||||
actorId,
|
actorId,
|
||||||
id,
|
id,
|
||||||
@@ -329,6 +370,7 @@ export const integrationServiceFactory = ({
|
|||||||
deleteIntegration,
|
deleteIntegration,
|
||||||
listIntegrationByProject,
|
listIntegrationByProject,
|
||||||
getIntegration,
|
getIntegration,
|
||||||
|
getIntegrationAWSIamRole,
|
||||||
syncIntegration
|
syncIntegration
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -280,7 +280,12 @@ export const projectMembershipServiceFactory = ({
|
|||||||
|
|
||||||
// validate custom roles input
|
// validate custom roles input
|
||||||
const customInputRoles = roles.filter(
|
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 hasCustomRole = Boolean(customInputRoles.length);
|
||||||
if (hasCustomRole) {
|
if (hasCustomRole) {
|
||||||
|
|||||||
@@ -191,6 +191,10 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
return project;
|
return project;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
throw new DatabaseError({ error, name: "Find all projects" });
|
throw new DatabaseError({ error, name: "Find all projects" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -240,6 +244,10 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
return project;
|
return project;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof NotFoundError || error instanceof UnauthorizedError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
throw new DatabaseError({ error, name: "Find project by slug" });
|
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" });
|
throw new BadRequestError({ message: "Invalid filter type" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError || error instanceof NotFoundError || error instanceof UnauthorizedError) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw new DatabaseError({ error, name: `Failed to find project by ${filter.type}` });
|
throw new DatabaseError({ error, name: `Failed to find project by ${filter.type}` });
|
||||||
|
|||||||
@@ -285,11 +285,14 @@ export const projectQueueFactory = ({
|
|||||||
|
|
||||||
if (!orgMembership) {
|
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.
|
// 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", {
|
logger.info(
|
||||||
userId: key.receiverId,
|
{
|
||||||
orgId: project.orgId,
|
userId: key.receiverId,
|
||||||
projectId: project.id
|
orgId: project.orgId,
|
||||||
});
|
projectId: project.id
|
||||||
|
},
|
||||||
|
"User is not in organization"
|
||||||
|
);
|
||||||
// eslint-disable-next-line no-continue
|
// eslint-disable-next-line no-continue
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -551,10 +554,10 @@ export const projectQueueFactory = ({
|
|||||||
.catch(() => [null]);
|
.catch(() => [null]);
|
||||||
|
|
||||||
if (!project) {
|
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 {
|
} else {
|
||||||
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed);
|
await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.Failed);
|
||||||
logger.error("Failed to upgrade project", err, {
|
logger.error(err, "Failed to upgrade project", {
|
||||||
extra: {
|
extra: {
|
||||||
project,
|
project,
|
||||||
jobData: data
|
jobData: data
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export const projectServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
|
workspaceDescription,
|
||||||
slug: projectSlug,
|
slug: projectSlug,
|
||||||
kmsKeyId,
|
kmsKeyId,
|
||||||
tx: trx,
|
tx: trx,
|
||||||
@@ -206,6 +207,7 @@ export const projectServiceFactory = ({
|
|||||||
const project = await projectDAL.create(
|
const project = await projectDAL.create(
|
||||||
{
|
{
|
||||||
name: workspaceName,
|
name: workspaceName,
|
||||||
|
description: workspaceDescription,
|
||||||
orgId: organization.id,
|
orgId: organization.id,
|
||||||
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
||||||
kmsSecretManagerKeyId: kmsKeyId,
|
kmsSecretManagerKeyId: kmsKeyId,
|
||||||
@@ -496,6 +498,7 @@ export const projectServiceFactory = ({
|
|||||||
|
|
||||||
const updatedProject = await projectDAL.updateById(project.id, {
|
const updatedProject = await projectDAL.updateById(project.id, {
|
||||||
name: update.name,
|
name: update.name,
|
||||||
|
description: update.description,
|
||||||
autoCapitalization: update.autoCapitalization
|
autoCapitalization: update.autoCapitalization
|
||||||
});
|
});
|
||||||
return updatedProject;
|
return updatedProject;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export type TCreateProjectDTO = {
|
|||||||
actorId: string;
|
actorId: string;
|
||||||
actorOrgId?: string;
|
actorOrgId?: string;
|
||||||
workspaceName: string;
|
workspaceName: string;
|
||||||
|
workspaceDescription?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
kmsKeyId?: string;
|
kmsKeyId?: string;
|
||||||
createDefaultEnvs?: boolean;
|
createDefaultEnvs?: boolean;
|
||||||
@@ -69,6 +70,7 @@ export type TUpdateProjectDTO = {
|
|||||||
filter: Filter;
|
filter: Filter;
|
||||||
update: {
|
update: {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
description?: string;
|
||||||
autoCapitalization?: boolean;
|
autoCapitalization?: boolean;
|
||||||
};
|
};
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export const fnTriggerWebhook = async ({
|
|||||||
!isDisabled && picomatch.isMatch(secretPath, hookSecretPath, { strictSlashes: false })
|
!isDisabled && picomatch.isMatch(secretPath, hookSecretPath, { strictSlashes: false })
|
||||||
);
|
);
|
||||||
if (!toBeTriggeredHooks.length) return;
|
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 project = await projectDAL.findById(projectId);
|
||||||
const webhooksTriggered = await Promise.allSettled(
|
const webhooksTriggered = await Promise.allSettled(
|
||||||
toBeTriggeredHooks.map((hook) =>
|
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");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ var exportCmd = &cobra.Command{
|
|||||||
accessToken = token.Token
|
accessToken = token.Token
|
||||||
} else {
|
} else {
|
||||||
log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
|
log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
|
||||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ var initCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userCreds, err := util.GetCurrentLoggedInUserDetails()
|
userCreds, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to get your login details")
|
util.HandleError(err, "Unable to get your login details")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ var loginCmd = &cobra.Command{
|
|||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
|
presetDomain := config.INFISICAL_URL
|
||||||
|
|
||||||
clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains")
|
clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
@@ -198,7 +200,7 @@ var loginCmd = &cobra.Command{
|
|||||||
|
|
||||||
// standalone user auth
|
// standalone user auth
|
||||||
if loginMethod == "user" {
|
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 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")) {
|
if err != nil && (strings.Contains(err.Error(), "we couldn't find your logged in details")) {
|
||||||
log.Debug().Err(err)
|
log.Debug().Err(err)
|
||||||
@@ -216,11 +218,19 @@ var loginCmd = &cobra.Command{
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usePresetDomain, err := usePresetDomain(presetDomain)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
//override domain
|
//override domain
|
||||||
domainQuery := true
|
domainQuery := true
|
||||||
if config.INFISICAL_URL_MANUAL_OVERRIDE != "" &&
|
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_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()
|
overrideDomain, err := DomainOverridePrompt()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
@@ -228,7 +238,7 @@ var loginCmd = &cobra.Command{
|
|||||||
|
|
||||||
//if not override set INFISICAL_URL to exported var
|
//if not override set INFISICAL_URL to exported var
|
||||||
//set domainQuery to false
|
//set domainQuery to false
|
||||||
if !overrideDomain {
|
if !overrideDomain && !usePresetDomain {
|
||||||
domainQuery = false
|
domainQuery = false
|
||||||
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL_MANUAL_OVERRIDE)
|
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL_MANUAL_OVERRIDE)
|
||||||
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", strings.TrimSuffix(config.INFISICAL_URL, "/api"))
|
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
|
//prompt user to select domain between Infisical cloud and self-hosting
|
||||||
if domainQuery {
|
if domainQuery && !usePresetDomain {
|
||||||
err = askForDomain()
|
err = askForDomain()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse domain url")
|
util.HandleError(err, "Unable to parse domain url")
|
||||||
@@ -526,6 +536,45 @@ func DomainOverridePrompt() (bool, error) {
|
|||||||
return selectedOption == OVERRIDE, err
|
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 {
|
func askForDomain() error {
|
||||||
|
|
||||||
// query user to choose between Infisical cloud or self-hosting
|
// query user to choose between Infisical cloud or self-hosting
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func init() {
|
|||||||
util.CheckForUpdate()
|
util.CheckForUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
loggedInDetails, err := util.GetCurrentLoggedInUserDetails()
|
loggedInDetails, err := util.GetCurrentLoggedInUserDetails(false)
|
||||||
|
|
||||||
if !silent && err == nil && loggedInDetails.IsUserLoggedIn && !loggedInDetails.LoginExpired {
|
if !silent && err == nil && loggedInDetails.IsUserLoggedIn && !loggedInDetails.LoginExpired {
|
||||||
token, err := util.GetInfisicalToken(cmd)
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
projectId = workspaceFile.WorkspaceId
|
projectId = workspaceFile.WorkspaceId
|
||||||
}
|
}
|
||||||
|
|
||||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "unable to authenticate [err=%v]")
|
util.HandleError(err, "unable to authenticate [err=%v]")
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ var secretsDeleteCmd = &cobra.Command{
|
|||||||
util.RequireLogin()
|
util.RequireLogin()
|
||||||
util.RequireLocalWorkspaceFile()
|
util.RequireLocalWorkspaceFile()
|
||||||
|
|
||||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to authenticate")
|
util.HandleError(err, "Unable to authenticate")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ var tokensCreateCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// get plain text workspace key
|
// get plain text workspace key
|
||||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to retrieve your logged in your details. Please login in then try again")
|
util.HandleError(err, "Unable to retrieve your logged in your details. Please login in then try again")
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func GetUserCredsFromKeyRing(userEmail string) (credentials models.UserCredentia
|
|||||||
return userCredentials, err
|
return userCredentials, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
|
func GetCurrentLoggedInUserDetails(setConfigVariables bool) (LoggedInUserDetails, error) {
|
||||||
if ConfigFileExists() {
|
if ConfigFileExists() {
|
||||||
configFile, err := GetConfigFile()
|
configFile, err := GetConfigFile()
|
||||||
if err != nil {
|
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
|
// check to to see if the JWT is still valid
|
||||||
httpClient := resty.New().
|
httpClient := resty.New().
|
||||||
SetAuthToken(userCreds.JTWToken).
|
SetAuthToken(userCreds.JTWToken).
|
||||||
SetHeader("Accept", "application/json")
|
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)
|
isAuthenticated := api.CallIsAuthenticated(httpClient)
|
||||||
// TODO: add refresh token
|
// TODO: add refresh token
|
||||||
// if !isAuthenticated {
|
// if !isAuthenticated {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder
|
|||||||
|
|
||||||
log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details")
|
log.Debug().Msg("GetAllFolders: Trying to fetch folders using logged in details")
|
||||||
|
|
||||||
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
|
loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -177,7 +177,7 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er
|
|||||||
if params.InfisicalToken == "" {
|
if params.InfisicalToken == "" {
|
||||||
RequireLogin()
|
RequireLogin()
|
||||||
RequireLocalWorkspaceFile()
|
RequireLocalWorkspaceFile()
|
||||||
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
|
loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return models.SingleFolder{}, err
|
return models.SingleFolder{}, err
|
||||||
@@ -224,7 +224,7 @@ func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder,
|
|||||||
RequireLogin()
|
RequireLogin()
|
||||||
RequireLocalWorkspaceFile()
|
RequireLocalWorkspaceFile()
|
||||||
|
|
||||||
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
|
loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
|
|||||||
|
|
||||||
log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
|
log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details")
|
||||||
|
|
||||||
loggedInUserDetails, err := GetCurrentLoggedInUserDetails()
|
loggedInUserDetails, err := GetCurrentLoggedInUserDetails(true)
|
||||||
isConnected := ValidateInfisicalAPIConnection()
|
isConnected := ValidateInfisicalAPIConnection()
|
||||||
|
|
||||||
if isConnected {
|
if isConnected {
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
---
|
||||||
|
title: "SAP ASE"
|
||||||
|
description: "Learn how to dynamically generate SAP ASE database account credentials."
|
||||||
|
---
|
||||||
|
|
||||||
|
The Infisical SAP ASE dynamic secret allows you to generate SAP ASE database credentials on demand.
|
||||||
|
|
||||||
|
## Prerequisite
|
||||||
|
|
||||||
|
- Infisical requires that you have a user in your SAP ASE instance, configured with the appropriate permissions. This user will facilitate the creation of new accounts as needed.
|
||||||
|
Ensure the user possesses privileges for creating, dropping, and granting permissions to roles for it to be able to create dynamic secrets.
|
||||||
|
The user used for authentication must have access to the `master` database. You can use the `sa` user for this purpose or create a new user with the necessary permissions.
|
||||||
|
|
||||||
|
- The SAP ASE instance should be reachable by Infisical.
|
||||||
|
|
||||||
|
## Set up Dynamic Secrets with SAP ASE
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Open Secret Overview Dashboard">
|
||||||
|
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||||
|
</Step>
|
||||||
|
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select SAP ASE">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Provide the inputs for dynamic secret parameters">
|
||||||
|
<ParamField path="Secret Name" type="string" required>
|
||||||
|
Name by which you want the secret to be referenced
|
||||||
|
</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)
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Max TTL" type="string" required>
|
||||||
|
The maximum time-to-live for a generated secret
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Host" type="string" required>
|
||||||
|
Your SAP ASE instance host (IP or domain)
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Port" type="number" required>
|
||||||
|
Your SAP ASE instance port. On default SAP ASE instances this is usually `5000`.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Database" type="number" required>
|
||||||
|
The database name that you want to generate credentials for. This database must exist on the SAP ASE instance.
|
||||||
|
Please note that the user/password used for authentication must have access to this database, **and** the `master` database.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="User" type="string" required>
|
||||||
|
Username that will be used to create dynamic secrets
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Password" type="string" required>
|
||||||
|
Password that will be used to create dynamic secrets
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="(Optional) Modify SQL Statements">
|
||||||
|
If you want to provide specific privileges for the generated dynamic credentials, you can modify the SQL statement to your needs.
|
||||||
|

|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Due to SAP ASE limitations, the attached SQL statements are not executed as a transaction.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Click 'Submit'">
|
||||||
|
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||||
|
</Step>
|
||||||
|
<Step title="Generate dynamic secrets">
|
||||||
|
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||||
|
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||||
|
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret in step 4.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
|
||||||
|
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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.
|
||||||
|

|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic
|
||||||
|
secret.
|
||||||
|
</Warning>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
@@ -188,6 +188,7 @@
|
|||||||
"documentation/platform/dynamic-secrets/mongo-db",
|
"documentation/platform/dynamic-secrets/mongo-db",
|
||||||
"documentation/platform/dynamic-secrets/azure-entra-id",
|
"documentation/platform/dynamic-secrets/azure-entra-id",
|
||||||
"documentation/platform/dynamic-secrets/ldap",
|
"documentation/platform/dynamic-secrets/ldap",
|
||||||
|
"documentation/platform/dynamic-secrets/sap-ase",
|
||||||
"documentation/platform/dynamic-secrets/sap-hana",
|
"documentation/platform/dynamic-secrets/sap-hana",
|
||||||
"documentation/platform/dynamic-secrets/snowflake",
|
"documentation/platform/dynamic-secrets/snowflake",
|
||||||
"documentation/platform/dynamic-secrets/totp"
|
"documentation/platform/dynamic-secrets/totp"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const path = require("path");
|
|||||||
|
|
||||||
const ContentSecurityPolicy = `
|
const ContentSecurityPolicy = `
|
||||||
default-src 'self';
|
default-src 'self';
|
||||||
connect-src 'self' https://*.posthog.com;
|
connect-src 'self' https://*.posthog.com http://127.0.0.1:*;
|
||||||
script-src 'self' https://*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
|
script-src 'self' https://*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
|
||||||
style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
|
style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
|
||||||
child-src https://api.stripe.com;
|
child-src https://api.stripe.com;
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
import { FC, useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalClose,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects,
|
||||||
|
useOrganization,
|
||||||
|
useOrgPermission,
|
||||||
|
useSubscription,
|
||||||
|
useUser
|
||||||
|
} from "@app/context";
|
||||||
|
import {
|
||||||
|
fetchOrgUsers,
|
||||||
|
useAddUserToWsNonE2EE,
|
||||||
|
useCreateWorkspace,
|
||||||
|
useGetExternalKmsList
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||||
|
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description too long, max length is 256 characters")
|
||||||
|
.optional(),
|
||||||
|
addMembers: z.boolean(),
|
||||||
|
kmsKeyId: z.string(),
|
||||||
|
template: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TAddProjectFormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
interface NewProjectModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NewProjectFormProps = Pick<NewProjectModalProps, "onOpenChange">;
|
||||||
|
|
||||||
|
const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { permission } = useOrgPermission();
|
||||||
|
const { user } = useUser();
|
||||||
|
const createWs = useCreateWorkspace();
|
||||||
|
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
|
const canReadProjectTemplates = permission.can(
|
||||||
|
OrgPermissionActions.Read,
|
||||||
|
OrgPermissionSubjects.ProjectTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: projectTemplates = [] } = useListProjectTemplates({
|
||||||
|
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!, {
|
||||||
|
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting, errors }
|
||||||
|
} = useForm<TAddProjectFormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
kmsKeyId: INTERNAL_KMS_KEY_ID,
|
||||||
|
template: InfisicalProjectTemplate.Default
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
console.log("Current form errors:", errors);
|
||||||
|
}
|
||||||
|
}, [errors]);
|
||||||
|
|
||||||
|
const onCreateProject = async ({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
addMembers,
|
||||||
|
kmsKeyId,
|
||||||
|
template
|
||||||
|
}: TAddProjectFormData) => {
|
||||||
|
// type check
|
||||||
|
if (!currentOrg) return;
|
||||||
|
if (!user) return;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
project: { id: newProjectId }
|
||||||
|
}
|
||||||
|
} = await createWs.mutateAsync({
|
||||||
|
projectName: name,
|
||||||
|
projectDescription: description,
|
||||||
|
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
||||||
|
template
|
||||||
|
});
|
||||||
|
|
||||||
|
if (addMembers) {
|
||||||
|
const orgUsers = await fetchOrgUsers(currentOrg.id);
|
||||||
|
await addUsersToProject.mutateAsync({
|
||||||
|
usernames: orgUsers
|
||||||
|
.filter(
|
||||||
|
(member) => member.user.username !== user.username && member.status === "accepted"
|
||||||
|
)
|
||||||
|
.map((member) => member.user.username),
|
||||||
|
projectId: newProjectId,
|
||||||
|
orgId: currentOrg.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
||||||
|
|
||||||
|
createNotification({ text: "Project created", type: "success" });
|
||||||
|
reset();
|
||||||
|
onOpenChange(false);
|
||||||
|
router.push(`/project/${newProjectId}/secrets/overview`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({ text: "Failed to create project", type: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onSubmit = handleSubmit((data) => {
|
||||||
|
return onCreateProject(data);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Project Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="Type your project name" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Project Description"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
isOptional
|
||||||
|
errorText={error?.message}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
placeholder="Project description"
|
||||||
|
{...field}
|
||||||
|
rows={3}
|
||||||
|
className="thin-scrollbar w-full !resize-none bg-mineshaft-900"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="template"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Read}
|
||||||
|
a={OrgPermissionSubjects.ProjectTemplates}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<FormControl
|
||||||
|
label="Project Template"
|
||||||
|
icon={<FontAwesomeIcon icon={faInfoCircle} size="sm" />}
|
||||||
|
tooltipText={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Create this project from a template to provision it with custom environments
|
||||||
|
and roles.
|
||||||
|
</p>
|
||||||
|
{subscription && !subscription.projectTemplates && (
|
||||||
|
<p className="pt-2">Project templates are a paid feature.</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={InfisicalProjectTemplate.Default}
|
||||||
|
placeholder={InfisicalProjectTemplate.Default}
|
||||||
|
isDisabled={!isAllowed || !subscription?.projectTemplates}
|
||||||
|
value={value}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{projectTemplates.length
|
||||||
|
? projectTemplates.map((template) => (
|
||||||
|
<SelectItem key={template.id} value={template.name}>
|
||||||
|
{template.name}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
: Object.values(InfisicalProjectTemplate).map((template) => (
|
||||||
|
<SelectItem key={template} value={template}>
|
||||||
|
{template}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 pl-1">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="addMembers"
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field: { onBlur, value, onChange } }) => (
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.Member}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<div>
|
||||||
|
<Checkbox
|
||||||
|
id="add-project-layout"
|
||||||
|
isChecked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onBlur={onBlur}
|
||||||
|
>
|
||||||
|
Add all members of my organization to this project
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-14 flex">
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="advance-settings" className="data-[state=open]:border-none">
|
||||||
|
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||||
|
<div className="order-1 ml-3">Advanced Settings</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<Controller
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl errorText={error?.message} isError={Boolean(error)} label="KMS">
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
className="mb-12 w-full bg-mineshaft-600"
|
||||||
|
>
|
||||||
|
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
|
||||||
|
Default Infisical KMS
|
||||||
|
</SelectItem>
|
||||||
|
{externalKmsList?.map((kms) => (
|
||||||
|
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
||||||
|
{kms.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="kmsKeyId"
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
<div className="absolute right-0 bottom-0 mr-6 mb-6 flex items-start justify-end">
|
||||||
|
<ModalClose>
|
||||||
|
<Button colorSchema="secondary" variant="plain" className="py-2">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
<Button isDisabled={isSubmitting} isLoading={isSubmitting} className="ml-4" type="submit">
|
||||||
|
Create Project
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NewProjectModal: FC<NewProjectModalProps> = ({ isOpen, onOpenChange }) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
title="Create a new project"
|
||||||
|
subTitle="This project will contain your secrets and configurations."
|
||||||
|
>
|
||||||
|
<NewProjectForm onOpenChange={onOpenChange} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { NewProjectModal } from "./NewProjectModal";
|
||||||
@@ -29,7 +29,8 @@ export enum DynamicSecretProviders {
|
|||||||
Ldap = "ldap",
|
Ldap = "ldap",
|
||||||
SapHana = "sap-hana",
|
SapHana = "sap-hana",
|
||||||
Snowflake = "snowflake",
|
Snowflake = "snowflake",
|
||||||
Totp = "totp"
|
Totp = "totp",
|
||||||
|
SapAse = "sap-ase"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SqlProviders {
|
export enum SqlProviders {
|
||||||
@@ -220,6 +221,18 @@ export type TDynamicSecretProvider =
|
|||||||
ca?: string | undefined;
|
ca?: string | undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: DynamicSecretProviders.SapAse;
|
||||||
|
inputs: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
database: string;
|
||||||
|
password: string;
|
||||||
|
creationStatement: string;
|
||||||
|
revocationStatement: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: DynamicSecretProviders.Snowflake;
|
type: DynamicSecretProviders.Snowflake;
|
||||||
inputs: {
|
inputs: {
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ export type TIntegration = {
|
|||||||
shouldEnableDelete?: boolean;
|
shouldEnableDelete?: boolean;
|
||||||
|
|
||||||
octopusDeployScopeValues?: TOctopusDeployScopeValues;
|
octopusDeployScopeValues?: TOctopusDeployScopeValues;
|
||||||
|
awsIamRole?: string;
|
||||||
|
region?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ export type {
|
|||||||
CreateWorkspaceDTO,
|
CreateWorkspaceDTO,
|
||||||
DeleteEnvironmentDTO,
|
DeleteEnvironmentDTO,
|
||||||
DeleteWorkspaceDTO,
|
DeleteWorkspaceDTO,
|
||||||
RenameWorkspaceDTO,
|
|
||||||
ToggleAutoCapitalizationDTO,
|
ToggleAutoCapitalizationDTO,
|
||||||
UpdateEnvironmentDTO,
|
UpdateEnvironmentDTO,
|
||||||
|
UpdateProjectDTO,
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceEnv,
|
WorkspaceEnv,
|
||||||
WorkspaceTag
|
WorkspaceTag
|
||||||
@@ -51,17 +51,26 @@ export enum ApiErrorTypes {
|
|||||||
|
|
||||||
export type TApiErrors =
|
export type TApiErrors =
|
||||||
| {
|
| {
|
||||||
|
requestId: string;
|
||||||
error: ApiErrorTypes.ValidationError;
|
error: ApiErrorTypes.ValidationError;
|
||||||
message: ZodIssue[];
|
message: ZodIssue[];
|
||||||
statusCode: 401;
|
statusCode: 401;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
requestId: string;
|
||||||
|
error: ApiErrorTypes.UnauthorizedError;
|
||||||
|
message: string;
|
||||||
|
statusCode: 401;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
requestId: string;
|
||||||
error: ApiErrorTypes.ForbiddenError;
|
error: ApiErrorTypes.ForbiddenError;
|
||||||
message: string;
|
message: string;
|
||||||
details: PureAbility["rules"];
|
details: PureAbility["rules"];
|
||||||
statusCode: 403;
|
statusCode: 403;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
requestId: string;
|
||||||
statusCode: 400;
|
statusCode: 400;
|
||||||
message: string;
|
message: string;
|
||||||
error: ApiErrorTypes.BadRequestError;
|
error: ApiErrorTypes.BadRequestError;
|
||||||
|
|||||||
@@ -33,10 +33,11 @@ export {
|
|||||||
useListWorkspacePkiAlerts,
|
useListWorkspacePkiAlerts,
|
||||||
useListWorkspacePkiCollections,
|
useListWorkspacePkiCollections,
|
||||||
useNameWorkspaceSecrets,
|
useNameWorkspaceSecrets,
|
||||||
useRenameWorkspace,
|
|
||||||
useToggleAutoCapitalization,
|
useToggleAutoCapitalization,
|
||||||
useUpdateIdentityWorkspaceRole,
|
useUpdateIdentityWorkspaceRole,
|
||||||
|
useUpdateProject,
|
||||||
useUpdateUserWorkspaceRole,
|
useUpdateUserWorkspaceRole,
|
||||||
useUpdateWsEnvironment,
|
useUpdateWsEnvironment,
|
||||||
useUpgradeProject} from "./queries";
|
useUpgradeProject
|
||||||
|
} from "./queries";
|
||||||
export { workspaceKeys } from "./query-keys";
|
export { workspaceKeys } from "./query-keys";
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
DeleteWorkspaceDTO,
|
DeleteWorkspaceDTO,
|
||||||
NameWorkspaceSecretsDTO,
|
NameWorkspaceSecretsDTO,
|
||||||
ProjectIdentityOrderBy,
|
ProjectIdentityOrderBy,
|
||||||
RenameWorkspaceDTO,
|
|
||||||
TGetUpgradeProjectStatusDTO,
|
TGetUpgradeProjectStatusDTO,
|
||||||
TListProjectIdentitiesDTO,
|
TListProjectIdentitiesDTO,
|
||||||
ToggleAutoCapitalizationDTO,
|
ToggleAutoCapitalizationDTO,
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
UpdateAuditLogsRetentionDTO,
|
UpdateAuditLogsRetentionDTO,
|
||||||
UpdateEnvironmentDTO,
|
UpdateEnvironmentDTO,
|
||||||
UpdatePitVersionLimitDTO,
|
UpdatePitVersionLimitDTO,
|
||||||
|
UpdateProjectDTO,
|
||||||
Workspace
|
Workspace
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -208,19 +208,26 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
|
|||||||
|
|
||||||
export const createWorkspace = ({
|
export const createWorkspace = ({
|
||||||
projectName,
|
projectName,
|
||||||
|
projectDescription,
|
||||||
kmsKeyId,
|
kmsKeyId,
|
||||||
template
|
template
|
||||||
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
|
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
|
||||||
return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId, template });
|
return apiRequest.post("/api/v2/workspace", {
|
||||||
|
projectName,
|
||||||
|
projectDescription,
|
||||||
|
kmsKeyId,
|
||||||
|
template
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateWorkspace = () => {
|
export const useCreateWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
|
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
|
||||||
mutationFn: async ({ projectName, kmsKeyId, template }) =>
|
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) =>
|
||||||
createWorkspace({
|
createWorkspace({
|
||||||
projectName,
|
projectName,
|
||||||
|
projectDescription,
|
||||||
kmsKeyId,
|
kmsKeyId,
|
||||||
template
|
template
|
||||||
}),
|
}),
|
||||||
@@ -230,12 +237,15 @@ export const useCreateWorkspace = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRenameWorkspace = () => {
|
export const useUpdateProject = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, RenameWorkspaceDTO>({
|
return useMutation<{}, {}, UpdateProjectDTO>({
|
||||||
mutationFn: ({ workspaceID, newWorkspaceName }) => {
|
mutationFn: ({ projectID, newProjectName, newProjectDescription }) => {
|
||||||
return apiRequest.post(`/api/v1/workspace/${workspaceID}/name`, { name: newWorkspaceName });
|
return apiRequest.patch(`/api/v1/workspace/${projectID}`, {
|
||||||
|
name: newProjectName,
|
||||||
|
description: newProjectDescription
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type Workspace = {
|
|||||||
__v: number;
|
__v: number;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description?: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
version: ProjectVersion;
|
version: ProjectVersion;
|
||||||
upgradeStatus: string | null;
|
upgradeStatus: string | null;
|
||||||
@@ -26,7 +27,6 @@ export type Workspace = {
|
|||||||
auditLogsRetentionDays: number;
|
auditLogsRetentionDays: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
||||||
roles?: TProjectRole[];
|
roles?: TProjectRole[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,11 +56,17 @@ export type TGetUpgradeProjectStatusDTO = {
|
|||||||
// mutation dto
|
// mutation dto
|
||||||
export type CreateWorkspaceDTO = {
|
export type CreateWorkspaceDTO = {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
|
projectDescription?: string;
|
||||||
kmsKeyId?: string;
|
kmsKeyId?: string;
|
||||||
template?: string;
|
template?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
export type UpdateProjectDTO = {
|
||||||
|
projectID: string;
|
||||||
|
newProjectName: string;
|
||||||
|
newProjectDescription?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
||||||
export type UpdateAuditLogsRetentionDTO = { projectSlug: string; auditLogsRetentionDays: number };
|
export type UpdateAuditLogsRetentionDTO = { projectSlug: string; auditLogsRetentionDays: number };
|
||||||
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
/* eslint-disable func-names */
|
/* eslint-disable func-names */
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -21,66 +20,48 @@ import {
|
|||||||
faEnvelope,
|
faEnvelope,
|
||||||
faInfinity,
|
faInfinity,
|
||||||
faInfo,
|
faInfo,
|
||||||
faInfoCircle,
|
|
||||||
faMobile,
|
faMobile,
|
||||||
faPlus,
|
faPlus,
|
||||||
faQuestion,
|
faQuestion,
|
||||||
faStar as faSolidStar
|
faStar as faSolidStar
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
|
||||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import * as yup from "yup";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
FormControl,
|
|
||||||
Input,
|
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
UpgradePlanModal
|
UpgradePlanModal
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
|
import { NewProjectModal } from "@app/components/v2/projects/NewProjectModal";
|
||||||
import {
|
import {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useOrgPermission,
|
|
||||||
useSubscription,
|
useSubscription,
|
||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
fetchOrgUsers,
|
|
||||||
useAddUserToWsNonE2EE,
|
|
||||||
useCreateWorkspace,
|
|
||||||
useGetAccessRequestsCount,
|
useGetAccessRequestsCount,
|
||||||
useGetExternalKmsList,
|
|
||||||
useGetOrgTrialUrl,
|
useGetOrgTrialUrl,
|
||||||
useGetSecretApprovalRequestCount,
|
useGetSecretApprovalRequestCount,
|
||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useSelectOrganization
|
useSelectOrganization
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
|
||||||
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
|
||||||
import { Workspace } from "@app/hooks/api/types";
|
import { Workspace } from "@app/hooks/api/types";
|
||||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||||
@@ -119,20 +100,6 @@ const supportOptions = [
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
const formSchema = yup.object({
|
|
||||||
name: yup
|
|
||||||
.string()
|
|
||||||
.required()
|
|
||||||
.label("Project Name")
|
|
||||||
.trim()
|
|
||||||
.max(64, "Too long, maximum length is 64 characters"),
|
|
||||||
addMembers: yup.bool().required().label("Add Members"),
|
|
||||||
kmsKeyId: yup.string().label("KMS Key ID"),
|
|
||||||
template: yup.string().label("Project Template Name")
|
|
||||||
});
|
|
||||||
|
|
||||||
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
|
||||||
|
|
||||||
export const AppLayout = ({ children }: LayoutProps) => {
|
export const AppLayout = ({ children }: LayoutProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -165,10 +132,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
|
|
||||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
||||||
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
|
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
|
||||||
const { permission } = useOrgPermission();
|
|
||||||
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!, {
|
|
||||||
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
|
|
||||||
});
|
|
||||||
|
|
||||||
const pendingRequestsCount = useMemo(() => {
|
const pendingRequestsCount = useMemo(() => {
|
||||||
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||||
@@ -178,27 +141,13 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
const createWs = useCreateWorkspace();
|
|
||||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
|
||||||
|
|
||||||
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
"addNewWs",
|
"addNewWs",
|
||||||
"upgradePlan",
|
"upgradePlan",
|
||||||
"createOrg"
|
"createOrg"
|
||||||
] as const);
|
] as const);
|
||||||
const {
|
|
||||||
control,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
reset,
|
|
||||||
handleSubmit
|
|
||||||
} = useForm<TAddProjectFormData>({
|
|
||||||
resolver: yupResolver(formSchema),
|
|
||||||
defaultValues: {
|
|
||||||
kmsKeyId: INTERNAL_KMS_KEY_ID
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -281,58 +230,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
putUserInOrg();
|
putUserInOrg();
|
||||||
}, [router.query.id]);
|
}, [router.query.id]);
|
||||||
|
|
||||||
const canReadProjectTemplates = permission.can(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.ProjectTemplates
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: projectTemplates = [] } = useListProjectTemplates({
|
|
||||||
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
|
|
||||||
});
|
|
||||||
|
|
||||||
const onCreateProject = async ({ name, addMembers, kmsKeyId, template }: TAddProjectFormData) => {
|
|
||||||
// type check
|
|
||||||
if (!currentOrg) return;
|
|
||||||
if (!user) return;
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
data: {
|
|
||||||
project: { id: newProjectId }
|
|
||||||
}
|
|
||||||
} = await createWs.mutateAsync({
|
|
||||||
projectName: name,
|
|
||||||
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
|
||||||
template
|
|
||||||
});
|
|
||||||
|
|
||||||
if (addMembers) {
|
|
||||||
const orgUsers = await fetchOrgUsers(currentOrg.id);
|
|
||||||
await addUsersToProject.mutateAsync({
|
|
||||||
usernames: orgUsers
|
|
||||||
.filter(
|
|
||||||
(member) => member.user.username !== user.username && member.status === "accepted"
|
|
||||||
)
|
|
||||||
.map((member) => member.user.username),
|
|
||||||
projectId: newProjectId,
|
|
||||||
orgId: currentOrg.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
||||||
|
|
||||||
createNotification({ text: "Project created", type: "success" });
|
|
||||||
handlePopUpClose("addNewWs");
|
|
||||||
router.push(`/project/${newProjectId}/secrets/overview`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({ text: "Failed to create project", type: "error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addProjectToFavorites = async (projectId: string) => {
|
const addProjectToFavorites = async (projectId: string) => {
|
||||||
try {
|
try {
|
||||||
if (currentOrg?.id) {
|
if (currentOrg?.id) {
|
||||||
@@ -560,8 +457,11 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
value={currentWorkspace?.id}
|
value={currentWorkspace?.id}
|
||||||
className="w-full bg-mineshaft-600 py-2.5 font-medium [&>*:first-child]:truncate"
|
className="w-full bg-mineshaft-600 py-2.5 font-medium [&>*:first-child]:truncate"
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
router.push(`/project/${value}/secrets/overview`);
|
|
||||||
localStorage.setItem("projectData.id", value);
|
localStorage.setItem("projectData.id", value);
|
||||||
|
// this is not using react query because react query in overview is throwing error when envs are not exact same count
|
||||||
|
// to reproduce change this back to router.push and switch between two projects with different env count
|
||||||
|
// look into this on dashboard revamp
|
||||||
|
window.location.assign(`/project/${value}/secrets/overview`);
|
||||||
}}
|
}}
|
||||||
position="popper"
|
position="popper"
|
||||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
|
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
|
||||||
@@ -916,176 +816,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
<Modal
|
<NewProjectModal
|
||||||
isOpen={popUp.addNewWs.isOpen}
|
isOpen={popUp.addNewWs.isOpen}
|
||||||
onOpenChange={(isModalOpen) => {
|
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||||
handlePopUpToggle("addNewWs", isModalOpen);
|
/>
|
||||||
reset();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
title="Create a new project"
|
|
||||||
subTitle="This project will contain your secrets and configurations."
|
|
||||||
>
|
|
||||||
<form onSubmit={handleSubmit(onCreateProject)}>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Project Name"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="Type your project name" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="template"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Read}
|
|
||||||
a={OrgPermissionSubjects.ProjectTemplates}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<FormControl
|
|
||||||
label="Project Template"
|
|
||||||
icon={<FontAwesomeIcon icon={faInfoCircle} size="sm" />}
|
|
||||||
tooltipText={
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
Create this project from a template to provision it with custom
|
|
||||||
environments and roles.
|
|
||||||
</p>
|
|
||||||
{subscription && !subscription.projectTemplates && (
|
|
||||||
<p className="pt-2">Project templates are a paid feature.</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={InfisicalProjectTemplate.Default}
|
|
||||||
placeholder={InfisicalProjectTemplate.Default}
|
|
||||||
isDisabled={!isAllowed || !subscription?.projectTemplates}
|
|
||||||
value={value}
|
|
||||||
onValueChange={onChange}
|
|
||||||
className="w-44"
|
|
||||||
>
|
|
||||||
{projectTemplates.length
|
|
||||||
? projectTemplates.map((template) => (
|
|
||||||
<SelectItem key={template.id} value={template.name}>
|
|
||||||
{template.name}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
: Object.values(InfisicalProjectTemplate).map((template) => (
|
|
||||||
<SelectItem key={template} value={template}>
|
|
||||||
{template}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 pl-1">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="addMembers"
|
|
||||||
defaultValue={false}
|
|
||||||
render={({ field: { onBlur, value, onChange } }) => (
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Read}
|
|
||||||
a={OrgPermissionSubjects.Member}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<div>
|
|
||||||
<Checkbox
|
|
||||||
id="add-project-layout"
|
|
||||||
isChecked={value}
|
|
||||||
onCheckedChange={onChange}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
onBlur={onBlur}
|
|
||||||
>
|
|
||||||
Add all members of my organization to this project
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-14 flex">
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
|
||||||
<AccordionItem
|
|
||||||
value="advance-settings"
|
|
||||||
className="data-[state=open]:border-none"
|
|
||||||
>
|
|
||||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
|
||||||
<div className="order-1 ml-3">Advanced Settings</div>
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<Controller
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
label="KMS"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => {
|
|
||||||
onChange(e);
|
|
||||||
}}
|
|
||||||
className="mb-12 w-full bg-mineshaft-600"
|
|
||||||
>
|
|
||||||
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
|
|
||||||
Default Infisical KMS
|
|
||||||
</SelectItem>
|
|
||||||
{externalKmsList?.map((kms) => (
|
|
||||||
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
|
||||||
{kms.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
control={control}
|
|
||||||
name="kmsKeyId"
|
|
||||||
/>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
<div className="absolute right-0 bottom-0 mr-6 mb-6 flex items-start justify-end">
|
|
||||||
<Button
|
|
||||||
key="layout-cancel-create-project"
|
|
||||||
onClick={() => handlePopUpClose("addNewWs")}
|
|
||||||
colorSchema="secondary"
|
|
||||||
variant="plain"
|
|
||||||
className="py-2"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
key="layout-create-project-submit"
|
|
||||||
className="ml-4"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Create Project
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
|
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -19,7 +18,6 @@ import {
|
|||||||
faExclamationCircle,
|
faExclamationCircle,
|
||||||
faFileShield,
|
faFileShield,
|
||||||
faHandPeace,
|
faHandPeace,
|
||||||
faInfoCircle,
|
|
||||||
faList,
|
faList,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faNetworkWired,
|
faNetworkWired,
|
||||||
@@ -29,48 +27,22 @@ import {
|
|||||||
faUserPlus
|
faUserPlus
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
|
||||||
import * as Tabs from "@radix-ui/react-tabs";
|
import * as Tabs from "@radix-ui/react-tabs";
|
||||||
import * as yup from "yup";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||||
import {
|
import { Button, IconButton, Input, Skeleton, UpgradePlanModal } from "@app/components/v2";
|
||||||
Accordion,
|
import { NewProjectModal } from "@app/components/v2/projects";
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Skeleton,
|
|
||||||
UpgradePlanModal
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
import {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useOrgPermission,
|
|
||||||
useSubscription,
|
useSubscription,
|
||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import {
|
import { useRegisterUserAction } from "@app/hooks/api";
|
||||||
fetchOrgUsers,
|
|
||||||
useAddUserToWsNonE2EE,
|
|
||||||
useCreateWorkspace,
|
|
||||||
useGetExternalKmsList,
|
|
||||||
useRegisterUserAction
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
|
||||||
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
|
||||||
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
import { Workspace } from "@app/hooks/api/types";
|
import { Workspace } from "@app/hooks/api/types";
|
||||||
@@ -476,20 +448,6 @@ const LearningItemSquare = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = yup.object({
|
|
||||||
name: yup
|
|
||||||
.string()
|
|
||||||
.required()
|
|
||||||
.label("Project Name")
|
|
||||||
.trim()
|
|
||||||
.max(64, "Too long, maximum length is 64 characters"),
|
|
||||||
addMembers: yup.bool().required().label("Add Members"),
|
|
||||||
kmsKeyId: yup.string().label("KMS Key ID"),
|
|
||||||
template: yup.string().label("Project Template Name")
|
|
||||||
});
|
|
||||||
|
|
||||||
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
|
||||||
|
|
||||||
// #TODO: Update all the workspaceIds
|
// #TODO: Update all the workspaceIds
|
||||||
const OrganizationPage = () => {
|
const OrganizationPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -498,7 +456,6 @@ const OrganizationPage = () => {
|
|||||||
|
|
||||||
const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace();
|
const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { permission } = useOrgPermission();
|
|
||||||
const routerOrgId = String(router.query.id);
|
const routerOrgId = String(router.query.id);
|
||||||
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || [];
|
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || [];
|
||||||
const { data: projectFavorites, isLoading: isProjectFavoritesLoading } =
|
const { data: projectFavorites, isLoading: isProjectFavoritesLoading } =
|
||||||
@@ -506,92 +463,25 @@ const OrganizationPage = () => {
|
|||||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||||
|
|
||||||
const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading;
|
const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading;
|
||||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
"addNewWs",
|
"addNewWs",
|
||||||
"upgradePlan"
|
"upgradePlan"
|
||||||
] as const);
|
] as const);
|
||||||
const {
|
|
||||||
control,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
reset,
|
|
||||||
handleSubmit
|
|
||||||
} = useForm<TAddProjectFormData>({
|
|
||||||
resolver: yupResolver(formSchema),
|
|
||||||
defaultValues: {
|
|
||||||
kmsKeyId: INTERNAL_KMS_KEY_ID
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
|
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
|
||||||
const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false);
|
const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false);
|
||||||
const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false);
|
const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false);
|
||||||
const [usersInOrg, setUsersInOrg] = useState(false);
|
const [usersInOrg, setUsersInOrg] = useState(false);
|
||||||
const [searchFilter, setSearchFilter] = useState("");
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
const createWs = useCreateWorkspace();
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { data: serverDetails } = useFetchServerStatus();
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
const [projectsViewMode, setProjectsViewMode] = useState<ProjectsViewMode>(
|
const [projectsViewMode, setProjectsViewMode] = useState<ProjectsViewMode>(
|
||||||
(localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID
|
(localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!, {
|
|
||||||
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
|
|
||||||
});
|
|
||||||
|
|
||||||
const onCreateProject = async ({ name, addMembers, kmsKeyId, template }: TAddProjectFormData) => {
|
|
||||||
// type check
|
|
||||||
if (!currentOrg) return;
|
|
||||||
if (!user) return;
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
data: {
|
|
||||||
project: { id: newProjectId }
|
|
||||||
}
|
|
||||||
} = await createWs.mutateAsync({
|
|
||||||
projectName: name,
|
|
||||||
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
|
||||||
template
|
|
||||||
});
|
|
||||||
|
|
||||||
if (addMembers) {
|
|
||||||
const orgUsers = await fetchOrgUsers(currentOrg.id);
|
|
||||||
|
|
||||||
await addUsersToProject.mutateAsync({
|
|
||||||
usernames: orgUsers
|
|
||||||
.filter(
|
|
||||||
(member) => member.user.username !== user.username && member.status === "accepted"
|
|
||||||
)
|
|
||||||
.map((member) => member.user.username),
|
|
||||||
projectId: newProjectId,
|
|
||||||
orgId: currentOrg.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
||||||
|
|
||||||
handlePopUpClose("addNewWs");
|
|
||||||
createNotification({ text: "Project created", type: "success" });
|
|
||||||
router.push(`/project/${newProjectId}/secrets/overview`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({ text: "Failed to create project", type: "error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const canReadProjectTemplates = permission.can(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.ProjectTemplates
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: projectTemplates = [] } = useListProjectTemplates({
|
|
||||||
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
|
|
||||||
});
|
|
||||||
|
|
||||||
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||||
: true;
|
: true;
|
||||||
@@ -669,7 +559,7 @@ const OrganizationPage = () => {
|
|||||||
localStorage.setItem("projectData.id", workspace.id);
|
localStorage.setItem("projectData.id", workspace.id);
|
||||||
}}
|
}}
|
||||||
key={workspace.id}
|
key={workspace.id}
|
||||||
className="min-w-72 flex h-40 cursor-pointer flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
className="min-w-72 flex h-40 cursor-pointer flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||||
>
|
>
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
|
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
|
||||||
@@ -693,18 +583,33 @@ const OrganizationPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
|
||||||
{workspace.environments?.length || 0} environments
|
<div
|
||||||
|
className="mt-1 mb-2.5 grow text-sm text-mineshaft-300"
|
||||||
|
style={{
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "-webkit-box",
|
||||||
|
WebkitBoxOrient: "vertical",
|
||||||
|
WebkitLineClamp: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{workspace.description}
|
||||||
</div>
|
</div>
|
||||||
<button type="button">
|
|
||||||
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 transition-all hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
|
<div className="flex w-full flex-row items-end justify-between place-self-end">
|
||||||
Explore{" "}
|
<div className="mt-0 text-xs text-mineshaft-400">
|
||||||
<FontAwesomeIcon
|
{workspace.environments?.length || 0} environments
|
||||||
icon={faArrowRight}
|
|
||||||
className="pl-1.5 pr-0.5 duration-200 hover:pl-2 hover:pr-0"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
<button type="button">
|
||||||
|
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 transition-all hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
|
||||||
|
Explore{" "}
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowRight}
|
||||||
|
className="pl-1.5 pr-0.5 duration-200 hover:pl-2 hover:pr-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1038,170 +943,10 @@ const OrganizationPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Modal
|
<NewProjectModal
|
||||||
isOpen={popUp.addNewWs.isOpen}
|
isOpen={popUp.addNewWs.isOpen}
|
||||||
onOpenChange={(isModalOpen) => {
|
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||||
handlePopUpToggle("addNewWs", isModalOpen);
|
/>
|
||||||
reset();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
title="Create a new project"
|
|
||||||
subTitle="This project will contain your secrets and configurations."
|
|
||||||
>
|
|
||||||
<form onSubmit={handleSubmit(onCreateProject)}>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Project Name"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="Type your project name" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="template"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Read}
|
|
||||||
a={OrgPermissionSubjects.ProjectTemplates}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<FormControl
|
|
||||||
label="Project Template"
|
|
||||||
icon={<FontAwesomeIcon icon={faInfoCircle} size="sm" />}
|
|
||||||
tooltipText={
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
Create this project from a template to provision it with custom
|
|
||||||
environments and roles.
|
|
||||||
</p>
|
|
||||||
{subscription && !subscription.projectTemplates && (
|
|
||||||
<p className="pt-2">Project templates are a paid feature.</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={InfisicalProjectTemplate.Default}
|
|
||||||
placeholder={InfisicalProjectTemplate.Default}
|
|
||||||
isDisabled={!isAllowed || !subscription?.projectTemplates}
|
|
||||||
value={value}
|
|
||||||
onValueChange={onChange}
|
|
||||||
className="w-44"
|
|
||||||
>
|
|
||||||
{projectTemplates.length
|
|
||||||
? projectTemplates.map((template) => (
|
|
||||||
<SelectItem key={template.id} value={template.name}>
|
|
||||||
{template.name}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
: Object.values(InfisicalProjectTemplate).map((template) => (
|
|
||||||
<SelectItem key={template} value={template}>
|
|
||||||
{template}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 pl-1">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="addMembers"
|
|
||||||
defaultValue={false}
|
|
||||||
render={({ field: { onBlur, value, onChange } }) => (
|
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.Member}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<div>
|
|
||||||
<Checkbox
|
|
||||||
id="add-project-layout"
|
|
||||||
isChecked={value}
|
|
||||||
onCheckedChange={onChange}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
onBlur={onBlur}
|
|
||||||
>
|
|
||||||
Add all members of my organization to this project
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-14 flex">
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
|
||||||
<AccordionItem value="advance-settings" className="data-[state=open]:border-none">
|
|
||||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
|
||||||
<div className="order-1 ml-3">Advanced Settings</div>
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
<Controller
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
label="KMS"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => {
|
|
||||||
onChange(e);
|
|
||||||
}}
|
|
||||||
className="mb-12 w-full bg-mineshaft-600"
|
|
||||||
>
|
|
||||||
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
|
|
||||||
Default Infisical KMS
|
|
||||||
</SelectItem>
|
|
||||||
{externalKmsList?.map((kms) => (
|
|
||||||
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
|
||||||
{kms.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
control={control}
|
|
||||||
name="kmsKeyId"
|
|
||||||
/>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
<div className="absolute right-0 bottom-0 mr-6 mb-6 flex items-start justify-end">
|
|
||||||
<Button
|
|
||||||
key="layout-cancel-create-project"
|
|
||||||
onClick={() => handlePopUpClose("addNewWs")}
|
|
||||||
colorSchema="secondary"
|
|
||||||
variant="plain"
|
|
||||||
className="py-2"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
key="layout-create-project-submit"
|
|
||||||
className="ml-4"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Create Project
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
|||||||
@@ -32,7 +32,12 @@ export const queryClient = new QueryClient({
|
|||||||
{
|
{
|
||||||
title: "Validation Error",
|
title: "Validation Error",
|
||||||
type: "error",
|
type: "error",
|
||||||
text: "Please check the input and try again.",
|
text: (
|
||||||
|
<div>
|
||||||
|
<p>Please check the input and try again.</p>
|
||||||
|
<p className="mt-2 text-xs">Request ID: {serverResponse.requestId}</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
children: (
|
children: (
|
||||||
<Modal>
|
<Modal>
|
||||||
<ModalTrigger>
|
<ModalTrigger>
|
||||||
@@ -72,7 +77,8 @@ export const queryClient = new QueryClient({
|
|||||||
{
|
{
|
||||||
title: "Forbidden Access",
|
title: "Forbidden Access",
|
||||||
type: "error",
|
type: "error",
|
||||||
text: serverResponse.message,
|
|
||||||
|
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`,
|
||||||
children: serverResponse?.details?.length ? (
|
children: serverResponse?.details?.length ? (
|
||||||
<Modal>
|
<Modal>
|
||||||
<ModalTrigger>
|
<ModalTrigger>
|
||||||
@@ -165,7 +171,11 @@ export const queryClient = new QueryClient({
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
createNotification({ title: "Bad Request", type: "error", text: serverResponse.message });
|
createNotification({
|
||||||
|
title: "Bad Request",
|
||||||
|
type: "error",
|
||||||
|
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
+3
-1
@@ -28,7 +28,9 @@ const metadataMappings: Record<keyof NonNullable<TIntegrationWithEnv["metadata"]
|
|||||||
shouldMaskSecrets: "GitLab Secrets Masking Enabled",
|
shouldMaskSecrets: "GitLab Secrets Masking Enabled",
|
||||||
shouldProtectSecrets: "GitLab Secret Protection Enabled",
|
shouldProtectSecrets: "GitLab Secret Protection Enabled",
|
||||||
shouldEnableDelete: "GitHub Secret Deletion Enabled",
|
shouldEnableDelete: "GitHub Secret Deletion Enabled",
|
||||||
octopusDeployScopeValues: "Octopus Deploy Scope Values"
|
octopusDeployScopeValues: "Octopus Deploy Scope Values",
|
||||||
|
awsIamRole: "AWS IAM Role",
|
||||||
|
region: "Region"
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const IntegrationSettingsSection = ({ integration }: Props) => {
|
export const IntegrationSettingsSection = ({ integration }: Props) => {
|
||||||
|
|||||||
@@ -63,32 +63,21 @@ export const IdentityClientSecretModal = ({ popUp, handlePopUpToggle }: Props) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFormSubmit = async ({ description, ttl, numUsesLimit }: FormData) => {
|
const onFormSubmit = async ({ description, ttl, numUsesLimit }: FormData) => {
|
||||||
try {
|
const { clientSecret } = await createClientSecret({
|
||||||
const { clientSecret } = await createClientSecret({
|
identityId: popUpData.identityId,
|
||||||
identityId: popUpData.identityId,
|
description,
|
||||||
description,
|
ttl: Number(ttl),
|
||||||
ttl: Number(ttl),
|
numUsesLimit: Number(numUsesLimit)
|
||||||
numUsesLimit: Number(numUsesLimit)
|
});
|
||||||
});
|
|
||||||
|
|
||||||
setToken(clientSecret);
|
setToken(clientSecret);
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully created client secret",
|
text: "Successfully created client secret",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
const error = err as any;
|
|
||||||
const text = error?.response?.data?.message ?? "Failed to create client secret";
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+25
-2
@@ -50,11 +50,34 @@ export const IdentityRoleDetailsSection = ({
|
|||||||
const handleRoleDelete = async () => {
|
const handleRoleDelete = async () => {
|
||||||
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
||||||
try {
|
try {
|
||||||
const updatedRole = identityMembershipDetails?.roles?.filter((el) => el.id !== id);
|
const updatedRoles = identityMembershipDetails?.roles?.filter((el) => el.id !== id);
|
||||||
await updateIdentityWorkspaceRole({
|
await updateIdentityWorkspaceRole({
|
||||||
workspaceId: currentWorkspace?.id || "",
|
workspaceId: currentWorkspace?.id || "",
|
||||||
identityId: identityMembershipDetails.identity.id,
|
identityId: identityMembershipDetails.identity.id,
|
||||||
roles: updatedRole
|
roles: updatedRoles.map(
|
||||||
|
({
|
||||||
|
role,
|
||||||
|
customRoleSlug,
|
||||||
|
isTemporary,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime
|
||||||
|
}) => ({
|
||||||
|
role: role === "custom" ? customRoleSlug : role,
|
||||||
|
...(isTemporary
|
||||||
|
? {
|
||||||
|
isTemporary,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
isTemporary
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
createNotification({ type: "success", text: "Successfully removed role" });
|
createNotification({ type: "success", text: "Successfully removed role" });
|
||||||
handlePopUpClose("deleteRole");
|
handlePopUpClose("deleteRole");
|
||||||
|
|||||||
+29
-3
@@ -61,10 +61,33 @@ export const MemberRoleDetailsSection = ({
|
|||||||
const handleRoleDelete = async () => {
|
const handleRoleDelete = async () => {
|
||||||
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
||||||
try {
|
try {
|
||||||
const updatedRole = membershipDetails?.roles?.filter((el) => el.id !== id);
|
const updatedRoles = membershipDetails?.roles?.filter((el) => el.id !== id);
|
||||||
await updateUserWorkspaceRole({
|
await updateUserWorkspaceRole({
|
||||||
workspaceId: currentWorkspace?.id || "",
|
workspaceId: currentWorkspace?.id || "",
|
||||||
roles: updatedRole,
|
roles: updatedRoles.map(
|
||||||
|
({
|
||||||
|
role,
|
||||||
|
customRoleSlug,
|
||||||
|
isTemporary,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime
|
||||||
|
}) => ({
|
||||||
|
role: role === "custom" ? customRoleSlug : role,
|
||||||
|
...(isTemporary
|
||||||
|
? {
|
||||||
|
isTemporary,
|
||||||
|
temporaryMode,
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessStartTime,
|
||||||
|
temporaryAccessEndTime
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
isTemporary
|
||||||
|
})
|
||||||
|
})
|
||||||
|
),
|
||||||
membershipId: membershipDetails.id
|
membershipId: membershipDetails.id
|
||||||
});
|
});
|
||||||
createNotification({ type: "success", text: "Successfully removed role" });
|
createNotification({ type: "success", text: "Successfully removed role" });
|
||||||
@@ -215,7 +238,10 @@ export const MemberRoleDetailsSection = ({
|
|||||||
title="Roles"
|
title="Roles"
|
||||||
subTitle="Select one or more of the pre-defined or custom roles to configure project permissions."
|
subTitle="Select one or more of the pre-defined or custom roles to configure project permissions."
|
||||||
>
|
>
|
||||||
<MemberRoleModify projectMember={membershipDetails} onOpenUpgradeModal={onOpenUpgradeModal} />
|
<MemberRoleModify
|
||||||
|
projectMember={membershipDetails}
|
||||||
|
onOpenUpgradeModal={onOpenUpgradeModal}
|
||||||
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+25
@@ -29,6 +29,7 @@ import { MongoAtlasInputForm } from "./MongoAtlasInputForm";
|
|||||||
import { MongoDBDatabaseInputForm } from "./MongoDBInputForm";
|
import { MongoDBDatabaseInputForm } from "./MongoDBInputForm";
|
||||||
import { RabbitMqInputForm } from "./RabbitMqInputForm";
|
import { RabbitMqInputForm } from "./RabbitMqInputForm";
|
||||||
import { RedisInputForm } from "./RedisInputForm";
|
import { RedisInputForm } from "./RedisInputForm";
|
||||||
|
import { SapAseInputForm } from "./SapAseInputForm";
|
||||||
import { SapHanaInputForm } from "./SapHanaInputForm";
|
import { SapHanaInputForm } from "./SapHanaInputForm";
|
||||||
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
|
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
|
||||||
import { TotpInputForm } from "./TotpInputForm";
|
import { TotpInputForm } from "./TotpInputForm";
|
||||||
@@ -107,6 +108,11 @@ const DYNAMIC_SECRET_LIST = [
|
|||||||
provider: DynamicSecretProviders.SapHana,
|
provider: DynamicSecretProviders.SapHana,
|
||||||
title: "SAP HANA"
|
title: "SAP HANA"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <SiSap size="1.5rem" />,
|
||||||
|
provider: DynamicSecretProviders.SapAse,
|
||||||
|
title: "SAP ASE"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: <SiSnowflake size="1.5rem" />,
|
icon: <SiSnowflake size="1.5rem" />,
|
||||||
provider: DynamicSecretProviders.Snowflake,
|
provider: DynamicSecretProviders.Snowflake,
|
||||||
@@ -393,6 +399,25 @@ export const CreateDynamicSecretForm = ({
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{wizardStep === WizardSteps.ProviderInputs &&
|
||||||
|
selectedProvider === DynamicSecretProviders.SapAse && (
|
||||||
|
<motion.div
|
||||||
|
key="dynamic-sap-ase-step"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
>
|
||||||
|
<SapAseInputForm
|
||||||
|
onCompleted={handleFormReset}
|
||||||
|
onCancel={handleFormReset}
|
||||||
|
projectSlug={projectSlug}
|
||||||
|
secretPath={secretPath}
|
||||||
|
environment={environment}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
{wizardStep === WizardSteps.ProviderInputs &&
|
{wizardStep === WizardSteps.ProviderInputs &&
|
||||||
selectedProvider === DynamicSecretProviders.Snowflake && (
|
selectedProvider === DynamicSecretProviders.Snowflake && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
+309
@@ -0,0 +1,309 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||||
|
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
provider: z.object({
|
||||||
|
host: z.string().toLowerCase().min(1),
|
||||||
|
port: z.coerce.number(),
|
||||||
|
database: z.string().min(1),
|
||||||
|
username: z.string().min(1),
|
||||||
|
password: z.string().min(1),
|
||||||
|
creationStatement: z.string().min(1),
|
||||||
|
revocationStatement: z.string().min(1)
|
||||||
|
}),
|
||||||
|
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
// a day
|
||||||
|
if (valMs > 24 * 60 * 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
maxTTL: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (!val) return;
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
// a day
|
||||||
|
if (valMs > 24 * 60 * 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||||
|
});
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onCompleted: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
secretPath: string;
|
||||||
|
projectSlug: string;
|
||||||
|
environment: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SapAseInputForm = ({
|
||||||
|
onCompleted,
|
||||||
|
onCancel,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
projectSlug
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
provider: {
|
||||||
|
database: "master",
|
||||||
|
port: 5000,
|
||||||
|
creationStatement: `sp_addlogin '{{username}}', '{{password}}';
|
||||||
|
sp_adduser '{{username}}', '{{username}}', null;
|
||||||
|
sp_role 'grant', 'mon_role', '{{username}}';`,
|
||||||
|
revocationStatement: `sp_dropuser '{{username}}';
|
||||||
|
sp_droplogin '{{username}}';`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDynamicSecret = useCreateDynamicSecret();
|
||||||
|
|
||||||
|
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||||
|
// wait till previous request is finished
|
||||||
|
if (createDynamicSecret.isLoading) return;
|
||||||
|
try {
|
||||||
|
await createDynamicSecret.mutateAsync({
|
||||||
|
provider: { type: DynamicSecretProviders.SapAse, inputs: provider },
|
||||||
|
maxTTL,
|
||||||
|
name,
|
||||||
|
path: secretPath,
|
||||||
|
defaultTTL,
|
||||||
|
projectSlug,
|
||||||
|
environmentSlug: environment
|
||||||
|
});
|
||||||
|
onCompleted();
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create dynamic secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="dynamic-secret" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-32">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="defaultTTL"
|
||||||
|
defaultValue="1h"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={<TtlFormLabel label="Default TTL" />}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-32">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="maxTTL"
|
||||||
|
defaultValue="24h"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={<TtlFormLabel label="Max TTL" />}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||||
|
Configuration
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.host"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Host"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input placeholder="92.41.22.72" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.port"
|
||||||
|
defaultValue={5000}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Port"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="number" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.database"
|
||||||
|
defaultValue="master"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Database"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="text" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.username"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="w-full"
|
||||||
|
label="User"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} autoComplete="off" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.password"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="w-full"
|
||||||
|
label="Password"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="password" autoComplete="new-password" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
|
||||||
|
<AccordionItem value="advance-statements">
|
||||||
|
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.creationStatement"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Creation Statement"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="username and password are dynamically provisioned. The sp_addlogin statement is automatically called against the master database. All other statements are called against the database you specify."
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
reSize="none"
|
||||||
|
rows={3}
|
||||||
|
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.revocationStatement"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Revocation Statement"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="username is dynamically provisioned. The sp_droplogin statement is automatically called against the master database. All other statements are called against the database you specify."
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
reSize="none"
|
||||||
|
rows={3}
|
||||||
|
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center space-x-4">
|
||||||
|
<Button type="submit" isLoading={isSubmitting}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+2
-1
@@ -136,7 +136,8 @@ const renderOutputForm = (
|
|||||||
provider === DynamicSecretProviders.SqlDatabase ||
|
provider === DynamicSecretProviders.SqlDatabase ||
|
||||||
provider === DynamicSecretProviders.Cassandra ||
|
provider === DynamicSecretProviders.Cassandra ||
|
||||||
provider === DynamicSecretProviders.MongoAtlas ||
|
provider === DynamicSecretProviders.MongoAtlas ||
|
||||||
provider === DynamicSecretProviders.MongoDB
|
provider === DynamicSecretProviders.MongoDB ||
|
||||||
|
provider === DynamicSecretProviders.SapAse
|
||||||
) {
|
) {
|
||||||
const { DB_PASSWORD, DB_USERNAME } = data as { DB_USERNAME: string; DB_PASSWORD: string };
|
const { DB_PASSWORD, DB_USERNAME } = data as { DB_USERNAME: string; DB_PASSWORD: string };
|
||||||
return (
|
return (
|
||||||
|
|||||||
+18
@@ -14,6 +14,7 @@ import { EditDynamicSecretMongoAtlasForm } from "./EditDynamicSecretMongoAtlasFo
|
|||||||
import { EditDynamicSecretMongoDBForm } from "./EditDynamicSecretMongoDBForm";
|
import { EditDynamicSecretMongoDBForm } from "./EditDynamicSecretMongoDBForm";
|
||||||
import { EditDynamicSecretRabbitMqForm } from "./EditDynamicSecretRabbitMqForm";
|
import { EditDynamicSecretRabbitMqForm } from "./EditDynamicSecretRabbitMqForm";
|
||||||
import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProviderForm";
|
import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProviderForm";
|
||||||
|
import { EditDynamicSecretSapAseForm } from "./EditDynamicSecretSapAseForm";
|
||||||
import { EditDynamicSecretSapHanaForm } from "./EditDynamicSecretSapHanaForm";
|
import { EditDynamicSecretSapHanaForm } from "./EditDynamicSecretSapHanaForm";
|
||||||
import { EditDynamicSecretSnowflakeForm } from "./EditDynamicSecretSnowflakeForm";
|
import { EditDynamicSecretSnowflakeForm } from "./EditDynamicSecretSnowflakeForm";
|
||||||
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
|
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
|
||||||
@@ -260,6 +261,23 @@ export const EditDynamicSecretForm = ({
|
|||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
{dynamicSecretDetails?.type === DynamicSecretProviders.SapAse && (
|
||||||
|
<motion.div
|
||||||
|
key="sap-ase-edit"
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
>
|
||||||
|
<EditDynamicSecretSapAseForm
|
||||||
|
onClose={onClose}
|
||||||
|
projectSlug={projectSlug}
|
||||||
|
secretPath={secretPath}
|
||||||
|
dynamicSecret={dynamicSecretDetails}
|
||||||
|
environment={environment}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
{dynamicSecretDetails?.type === DynamicSecretProviders.Snowflake && (
|
{dynamicSecretDetails?.type === DynamicSecretProviders.Snowflake && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="snowflake-edit"
|
key="snowflake-edit"
|
||||||
|
|||||||
+331
@@ -0,0 +1,331 @@
|
|||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import ms from "ms";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
SecretInput,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||||
|
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
inputs: z
|
||||||
|
.object({
|
||||||
|
host: z.string().toLowerCase().min(1),
|
||||||
|
port: z.coerce.number(),
|
||||||
|
database: z.string().min(1),
|
||||||
|
username: z.string().min(1),
|
||||||
|
password: z.string().min(1),
|
||||||
|
creationStatement: z.string().min(1),
|
||||||
|
revocationStatement: z.string().min(1),
|
||||||
|
ca: z.string().optional()
|
||||||
|
})
|
||||||
|
.partial(),
|
||||||
|
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
// a day
|
||||||
|
if (valMs > 24 * 60 * 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
maxTTL: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (!val) return;
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
|
// a day
|
||||||
|
if (valMs > 24 * 60 * 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
newName: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||||
|
});
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void;
|
||||||
|
dynamicSecret: TDynamicSecret & { inputs: unknown };
|
||||||
|
secretPath: string;
|
||||||
|
projectSlug: string;
|
||||||
|
environment: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditDynamicSecretSapAseForm = ({
|
||||||
|
onClose,
|
||||||
|
dynamicSecret,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
projectSlug
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit
|
||||||
|
} = useForm<TForm>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
values: {
|
||||||
|
defaultTTL: dynamicSecret.defaultTTL,
|
||||||
|
maxTTL: dynamicSecret.maxTTL,
|
||||||
|
newName: dynamicSecret.name,
|
||||||
|
inputs: {
|
||||||
|
...(dynamicSecret.inputs as TForm["inputs"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||||
|
|
||||||
|
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||||
|
// wait till previous request is finished
|
||||||
|
if (updateDynamicSecret.isLoading) return;
|
||||||
|
try {
|
||||||
|
await updateDynamicSecret.mutateAsync({
|
||||||
|
name: dynamicSecret.name,
|
||||||
|
path: secretPath,
|
||||||
|
projectSlug,
|
||||||
|
environmentSlug: environment,
|
||||||
|
data: {
|
||||||
|
maxTTL: maxTTL || undefined,
|
||||||
|
defaultTTL,
|
||||||
|
inputs,
|
||||||
|
newName: newName === dynamicSecret.name ? undefined : newName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully updated dynamic secret"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update dynamic secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="newName"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Secret Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="dynamic-secret" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-32">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="defaultTTL"
|
||||||
|
defaultValue="1h"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={<TtlFormLabel label="Default TTL" />}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-32">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="maxTTL"
|
||||||
|
defaultValue="24h"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={<TtlFormLabel label="Max TTL" />}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||||
|
Configuration
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.host"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Host"
|
||||||
|
className="flex-grow"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.port"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Port"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="number" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.database"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Database"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="text" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.username"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="w-full"
|
||||||
|
label="User"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} autoComplete="off" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.password"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="w-full"
|
||||||
|
label="Password"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="password" autoComplete="new-password" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.ca"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isOptional
|
||||||
|
label="CA(SSL)"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<SecretInput
|
||||||
|
{...field}
|
||||||
|
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
|
||||||
|
<AccordionItem value="advance-statements">
|
||||||
|
<AccordionTrigger>Modify SQL Statements</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.creationStatement"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Creation Statement"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="username and password are dynamically provisioned. The sp_addlogin statement is automatically called against the master database. All other statements are called against the database you specify."
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
reSize="none"
|
||||||
|
rows={3}
|
||||||
|
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.revocationStatement"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Revocation Statement"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
helperText="username is dynamically provisioned. The sp_droplogin statement is automatically called against the master database. All other statements are called against the database you specify."
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
reSize="none"
|
||||||
|
rows={3}
|
||||||
|
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center space-x-4">
|
||||||
|
<Button type="submit" isLoading={isSubmitting}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -186,7 +186,7 @@ export const SecretOverviewPage = () => {
|
|||||||
useGetImportedSecretsAllEnvs({
|
useGetImportedSecretsAllEnvs({
|
||||||
projectId: workspaceId,
|
projectId: workspaceId,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
environments: userAvailableEnvs.map(({ slug }) => slug)
|
environments: (userAvailableEnvs || []).map(({ slug }) => slug)
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isLoading: isOverviewLoading, data: overview } = useGetProjectSecretsOverview(
|
const { isLoading: isOverviewLoading, data: overview } = useGetProjectSecretsOverview(
|
||||||
@@ -618,7 +618,7 @@ export const SecretOverviewPage = () => {
|
|||||||
}
|
}
|
||||||
}, [router.query.search]);
|
}, [router.query.search]);
|
||||||
|
|
||||||
if (isWorkspaceLoading || (isProjectV3 && isOverviewLoading)) {
|
if (isWorkspaceLoading || (isProjectV3 && visibleEnvs.length > 0 && isOverviewLoading)) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<img
|
<img
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@ import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSect
|
|||||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||||
import { EnvironmentSection } from "../EnvironmentSection";
|
import { EnvironmentSection } from "../EnvironmentSection";
|
||||||
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
|
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
|
||||||
import { ProjectNameChangeSection } from "../ProjectNameChangeSection";
|
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
||||||
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
|
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
|
||||||
import { SecretTagsSection } from "../SecretTagsSection";
|
import { SecretTagsSection } from "../SecretTagsSection";
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export const ProjectGeneralTab = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ProjectNameChangeSection />
|
<ProjectOverviewChangeSection />
|
||||||
<EnvironmentSection />
|
<EnvironmentSection />
|
||||||
<SecretTagsSection />
|
<SecretTagsSection />
|
||||||
<AutoCapitalizationSection />
|
<AutoCapitalizationSection />
|
||||||
|
|||||||
-119
@@ -1,119 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
|
||||||
import * as yup from "yup";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
||||||
import { Button, FormControl, Input } from "@app/components/v2";
|
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
|
||||||
import { useRenameWorkspace } from "@app/hooks/api";
|
|
||||||
|
|
||||||
import { CopyButton } from "./CopyButton";
|
|
||||||
|
|
||||||
const formSchema = yup.object({
|
|
||||||
name: yup
|
|
||||||
.string()
|
|
||||||
.required()
|
|
||||||
.label("Project Name")
|
|
||||||
.max(64, "Too long, maximum length is 64 characters")
|
|
||||||
});
|
|
||||||
|
|
||||||
type FormData = yup.InferType<typeof formSchema>;
|
|
||||||
|
|
||||||
export const ProjectNameChangeSection = () => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { mutateAsync, isLoading } = useRenameWorkspace();
|
|
||||||
|
|
||||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentWorkspace) {
|
|
||||||
reset({
|
|
||||||
name: currentWorkspace.name
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [currentWorkspace]);
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ name }: FormData) => {
|
|
||||||
try {
|
|
||||||
if (!currentWorkspace?.id) return;
|
|
||||||
|
|
||||||
await mutateAsync({
|
|
||||||
workspaceID: currentWorkspace.id,
|
|
||||||
newWorkspaceName: name
|
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully renamed workspace",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to rename workspace",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
|
||||||
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
|
||||||
>
|
|
||||||
<div className="justify-betweens flex">
|
|
||||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Name</h2>
|
|
||||||
<div className="space-x-2">
|
|
||||||
<CopyButton
|
|
||||||
value={currentWorkspace?.slug || ""}
|
|
||||||
hoverText="Click to project slug"
|
|
||||||
notificationText="Copied project slug to clipboard"
|
|
||||||
>
|
|
||||||
Copy Project Slug
|
|
||||||
</CopyButton>
|
|
||||||
<CopyButton
|
|
||||||
value={currentWorkspace?.id || ""}
|
|
||||||
hoverText="Click to project ID"
|
|
||||||
notificationText="Copied project ID to clipboard"
|
|
||||||
>
|
|
||||||
Copy Project ID
|
|
||||||
</CopyButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="max-w-md">
|
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Controller
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
|
||||||
<Input
|
|
||||||
placeholder="Project name"
|
|
||||||
{...field}
|
|
||||||
className="bg-mineshaft-800"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isLoading}
|
|
||||||
isDisabled={isLoading || !isAllowed}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export { ProjectNameChangeSection } from "./ProjectNameChangeSection";
|
|
||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import { useUpdateProject } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { CopyButton } from "./CopyButton";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description too long, max length is 256 characters")
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const ProjectOverviewChangeSection = () => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { mutateAsync, isLoading } = useUpdateProject();
|
||||||
|
|
||||||
|
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentWorkspace) {
|
||||||
|
reset({
|
||||||
|
name: currentWorkspace.name,
|
||||||
|
description: currentWorkspace.description ?? ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [currentWorkspace]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ name, description }: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!currentWorkspace?.id) return;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
projectID: currentWorkspace.id,
|
||||||
|
newProjectName: name,
|
||||||
|
newProjectDescription: description
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated project overview",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update project overview",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="justify-betweens flex">
|
||||||
|
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<CopyButton
|
||||||
|
value={currentWorkspace?.slug || ""}
|
||||||
|
hoverText="Click to project slug"
|
||||||
|
notificationText="Copied project slug to clipboard"
|
||||||
|
>
|
||||||
|
Copy Project Slug
|
||||||
|
</CopyButton>
|
||||||
|
<CopyButton
|
||||||
|
value={currentWorkspace?.id || ""}
|
||||||
|
hoverText="Click to project ID"
|
||||||
|
notificationText="Copied project ID to clipboard"
|
||||||
|
>
|
||||||
|
Copy Project ID
|
||||||
|
</CopyButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col gap-0">
|
||||||
|
<div className="flex w-full flex-row items-end gap-4">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Project}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Project name"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Project name"
|
||||||
|
{...field}
|
||||||
|
className=" bg-mineshaft-800"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-row items-end gap-4">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Project}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Project description"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
placeholder="Project description"
|
||||||
|
{...field}
|
||||||
|
rows={3}
|
||||||
|
className="thin-scrollbar max-w-md !resize-none bg-mineshaft-800"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Project}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={isLoading || !isAllowed}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||||
@@ -2,5 +2,5 @@ export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
|
|||||||
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
|
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
|
||||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||||
export { EnvironmentSection } from "./EnvironmentSection";
|
export { EnvironmentSection } from "./EnvironmentSection";
|
||||||
export { ProjectNameChangeSection } from "./ProjectNameChangeSection";
|
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||||
export { SecretTagsSection } from "./SecretTagsSection";
|
export { SecretTagsSection } from "./SecretTagsSection";
|
||||||
|
|||||||
Reference in New Issue
Block a user