mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-21 22:22:26 +00:00
Compare commits
94 Commits
doc/add-ab
...
infisical-
Author | SHA1 | Date | |
---|---|---|---|
53f6ab118b | |||
0f5a1b13a6 | |||
e004be22e3 | |||
016cb4a7ba | |||
9bfc2a5dd2 | |||
f376eaae13 | |||
026f883d21 | |||
e42f860261 | |||
1512d4f496 | |||
9f7b42ad91 | |||
3045477c32 | |||
4eba80905a | |||
b023bc7442 | |||
a0029ab469 | |||
53605c3880 | |||
e5bca5b5df | |||
4091bc19e9 | |||
23bd048bb9 | |||
17a4674821 | |||
ec9631107d | |||
3fa450b9a7 | |||
3b9c62c366 | |||
cb3d171d48 | |||
c29841fbcf | |||
fcccf1bd8d | |||
4382825162 | |||
f80ef1dcc8 | |||
7abf3e3642 | |||
82ef35bd08 | |||
4eb668b5a5 | |||
18edea9f26 | |||
787c091948 | |||
ff269b1063 | |||
ca0636cb25 | |||
b995358b7e | |||
7aaf0f4ed3 | |||
68646bcdf8 | |||
9989ceb6d1 | |||
95d7ba5f22 | |||
2aa6fdf983 | |||
be5a32a5d6 | |||
f009cd329b | |||
e2778864e2 | |||
ea7375b2c6 | |||
d42566c335 | |||
45cbd9f006 | |||
8580602ea7 | |||
7ff75cdfab | |||
bd8c8871c0 | |||
d5aa13b277 | |||
428dc5d371 | |||
f1facf1f2c | |||
31dc36d4e2 | |||
51f29e5357 | |||
30f0f174d1 | |||
3e7110f334 | |||
e6af7a6fb9 | |||
de420fd02c | |||
41a3ca149d | |||
da38d1a261 | |||
b0d8c8fb23 | |||
d84bac5fba | |||
44f74e4d12 | |||
c16a4e00d8 | |||
11f2719842 | |||
f8153dd896 | |||
b104f8c07d | |||
746687e5b5 | |||
080b1e1550 | |||
38a6fd140c | |||
19d66abc38 | |||
e61c0be6db | |||
917573931f | |||
929a41065c | |||
9b44972e77 | |||
17e576511b | |||
afd444cad6 | |||
55b1fbdf52 | |||
46ca5c8efa | |||
f7406ea8f8 | |||
f34370cb9d | |||
78718cd299 | |||
1307fa49d4 | |||
a7ca242f5d | |||
c6b3b24312 | |||
8520029958 | |||
7905017121 | |||
4bbe80c083 | |||
d65ae2c61b | |||
84c534ef70 | |||
ce4c5d8ea1 | |||
617aa2f533 | |||
e9dd3340bf | |||
1c2b4e91ba |
.env.exampleDockerfile.fips.standalone-infisicalDockerfile.standalone-infisicalmint.json
backend
Dockerfile.devpackage-lock.jsonpackage.json
src
@types
db
knexfile.ts
migrations
20250212191958_create-gateway.ts20250226021631_secret-requests.ts20250226082254_add-gov-banner-and-consent-fields.ts20250228022604_increase-secret-reminder-note-max-length.ts20250303213350_add-folder-description.ts
schemas
ee
routes/v1
services
audit-log
dynamic-secret
gateway
license
permission
secret-approval-request
ssh-certificate
keystore
lib
api-docs
config
crypto
gateway
telemetry
turn
server/routes
services
resource-cleanup
secret-folder
secret-sharing
smtp
telemetry
cli
company/handbook
docker-compose.prod.ymldocs
api-reference/overview
documentation/platform
images/platform
admin-panels
gateways
integrations
platforms/kubernetes
secret-syncs
frontend
package-lock.jsonpackage.jsonmain.tsxrouteTree.gen.tsroutes.ts
public/lotties
src
components
const.tsconst
context/OrgPermissionContext
hooks
layouts
AdminLayout
OrganizationLayout
pages
admin/OverviewPage
middlewares
organization
Gateways/GatewayListPage
RoleByIDPage/components
OrgRoleModifySection.utils.ts
RolePermissionsSection
SecretSharingPage
public/ViewSecretRequestByIDPage
ViewSecretRequestByIDPage.tsx
components
SecretErrorContainer.tsxSecretRequestContainer.tsxSecretRequestSuccessContainer.tsxSecretValueAlreadySharedContainer.tsx
route.tsxsecret-manager
OverviewPage
SecretDashboardPage/components
ActionBar
DynamicSecretListView/EditDynamicSecretForm
FolderListView
SecretListView
integrations/BitbucketConfigurePage
helm-charts/secrets-operator
k8-operator
config/rbac
controllers/infisicalsecret
kubectl-install
packages/controllerhelpers
sink
@ -112,4 +112,11 @@ INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=
|
||||
|
||||
# azure app connection
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_ID=
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRET=
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRET=
|
||||
|
||||
# datadog
|
||||
SHOULD_USE_DATADOG_TRACER=
|
||||
DATADOG_PROFILING_ENABLED=
|
||||
DATADOG_ENV=
|
||||
DATADOG_SERVICE=
|
||||
DATADOG_HOSTNAME=
|
||||
|
@ -161,6 +161,9 @@ COPY --from=backend-runner /app /backend
|
||||
|
||||
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||
|
||||
ARG INFISICAL_PLATFORM_VERSION
|
||||
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||
|
||||
ENV PORT 8080
|
||||
ENV HOST=0.0.0.0
|
||||
ENV HTTPS_ENABLED false
|
||||
|
@ -3,13 +3,10 @@ ARG POSTHOG_API_KEY=posthog-api-key
|
||||
ARG INTERCOM_ID=intercom-id
|
||||
ARG CAPTCHA_SITE_KEY=captcha-site-key
|
||||
|
||||
FROM node:20-alpine AS base
|
||||
FROM node:20-slim AS base
|
||||
|
||||
FROM base AS frontend-dependencies
|
||||
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
@ -45,8 +42,8 @@ RUN npm run build
|
||||
FROM base AS frontend-runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 non-root-user
|
||||
RUN groupadd --system --gid 1001 nodejs
|
||||
RUN useradd --system --uid 1001 --gid nodejs non-root-user
|
||||
|
||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/dist ./
|
||||
|
||||
@ -56,21 +53,23 @@ USER non-root-user
|
||||
## BACKEND
|
||||
##
|
||||
FROM base AS backend-build
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 non-root-user
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install all required dependencies for build
|
||||
RUN apk --update add \
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
unixodbc \
|
||||
freetds \
|
||||
freetds-bin \
|
||||
unixodbc-dev \
|
||||
libc-dev \
|
||||
freetds-dev
|
||||
freetds-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN groupadd --system --gid 1001 nodejs
|
||||
RUN useradd --system --uid 1001 --gid nodejs non-root-user
|
||||
|
||||
COPY backend/package*.json ./
|
||||
RUN npm ci --only-production
|
||||
@ -86,18 +85,19 @@ FROM base AS backend-runner
|
||||
WORKDIR /app
|
||||
|
||||
# Install all required dependencies for runtime
|
||||
RUN apk --update add \
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
unixodbc \
|
||||
freetds \
|
||||
freetds-bin \
|
||||
unixodbc-dev \
|
||||
libc-dev \
|
||||
freetds-dev
|
||||
freetds-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Configure ODBC
|
||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/libtdsodbc.so\nSetup = /usr/lib/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||
|
||||
COPY backend/package*.json ./
|
||||
RUN npm ci --only-production
|
||||
@ -109,34 +109,35 @@ RUN mkdir frontend-build
|
||||
# Production stage
|
||||
FROM base AS production
|
||||
|
||||
RUN apk add --upgrade --no-cache ca-certificates
|
||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
||||
&& apk add infisical=0.31.1 && apk add --no-cache git
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# Install all required runtime dependencies
|
||||
RUN apk --update add \
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
bash \
|
||||
curl \
|
||||
git \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
unixodbc \
|
||||
freetds \
|
||||
freetds-bin \
|
||||
unixodbc-dev \
|
||||
libc-dev \
|
||||
freetds-dev \
|
||||
bash \
|
||||
curl \
|
||||
git \
|
||||
openssh
|
||||
openssh-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
|
||||
&& apt-get update && apt-get install -y infisical=0.31.1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
# Setup user permissions
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 non-root-user
|
||||
RUN groupadd --system --gid 1001 nodejs \
|
||||
&& useradd --system --uid 1001 --gid nodejs non-root-user
|
||||
|
||||
# Give non-root-user permission to update SSL certs
|
||||
RUN chown -R non-root-user /etc/ssl/certs
|
||||
@ -154,11 +155,11 @@ ENV INTERCOM_ID=$INTERCOM_ID
|
||||
ARG CAPTCHA_SITE_KEY
|
||||
ENV CAPTCHA_SITE_KEY=$CAPTCHA_SITE_KEY
|
||||
|
||||
|
||||
COPY --from=backend-runner /app /backend
|
||||
|
||||
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||
|
||||
ARG INFISICAL_PLATFORM_VERSION
|
||||
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||
|
||||
ENV PORT 8080
|
||||
ENV HOST=0.0.0.0
|
||||
@ -166,6 +167,7 @@ ENV HTTPS_ENABLED false
|
||||
ENV NODE_ENV production
|
||||
ENV STANDALONE_BUILD true
|
||||
ENV STANDALONE_MODE true
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
ENV TELEMETRY_ENABLED true
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine
|
||||
FROM node:20-slim
|
||||
|
||||
# ? Setup a test SoftHSM module. In production a real HSM is used.
|
||||
|
||||
@ -7,32 +7,32 @@ ARG SOFTHSM2_VERSION=2.5.0
|
||||
ENV SOFTHSM2_VERSION=${SOFTHSM2_VERSION} \
|
||||
SOFTHSM2_SOURCES=/tmp/softhsm2
|
||||
|
||||
# install build dependencies including python3 (required for pkcs11js and partially TDS driver)
|
||||
RUN apk --update add \
|
||||
alpine-sdk \
|
||||
autoconf \
|
||||
automake \
|
||||
git \
|
||||
libtool \
|
||||
openssl-dev \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
openssh
|
||||
# Install build dependencies including python3 (required for pkcs11js and partially TDS driver)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
autoconf \
|
||||
automake \
|
||||
git \
|
||||
libtool \
|
||||
libssl-dev \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
openssh-client \
|
||||
curl \
|
||||
pkg-config
|
||||
|
||||
# install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||
RUN apk add --no-cache \
|
||||
# Install dependencies for TDS driver (required for SAP ASE dynamic secrets)
|
||||
RUN apt-get install -y \
|
||||
unixodbc \
|
||||
freetds \
|
||||
unixodbc-dev \
|
||||
libc-dev \
|
||||
freetds-dev
|
||||
freetds-dev \
|
||||
freetds-bin \
|
||||
tdsodbc
|
||||
|
||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||
|
||||
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}
|
||||
WORKDIR ${SOFTHSM2_SOURCES}
|
||||
|
||||
@ -45,16 +45,18 @@ RUN git checkout ${SOFTHSM2_VERSION} -b ${SOFTHSM2_VERSION} \
|
||||
WORKDIR /root
|
||||
RUN rm -fr ${SOFTHSM2_SOURCES}
|
||||
|
||||
# install pkcs11-tool
|
||||
RUN apk --update add opensc
|
||||
# Install pkcs11-tool
|
||||
RUN apt-get install -y opensc
|
||||
|
||||
RUN softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
|
||||
RUN mkdir -p /etc/softhsm2/tokens && \
|
||||
softhsm2-util --init-token --slot 0 --label "auth-app" --pin 1234 --so-pin 0000
|
||||
|
||||
# ? App setup
|
||||
|
||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
||||
&& apk add infisical=0.8.1 && apk add --no-cache git
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \
|
||||
apt-get update && \
|
||||
apt-get install -y infisical=0.8.1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
2239
backend/package-lock.json
generated
2239
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -60,6 +60,13 @@
|
||||
"migration:status": "npm run auditlog-migration:status && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:status",
|
||||
"migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./dist/db/knexfile.mjs migrate:rollback",
|
||||
"migration:unlock": "npm run auditlog-migration:unlock && knex --knexfile ./dist/db/knexfile.mjs migrate:unlock",
|
||||
"migration:up-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
|
||||
"migration:down-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
|
||||
"migration:list-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
|
||||
"migration:latest-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||
"migration:status-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
|
||||
"migration:rollback-dev": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
||||
"migration:unlock-dev": "knex --knexfile ./src/db/knexfile.ts migrate:unlock",
|
||||
"migrate:org": "tsx ./scripts/migrate-organization.ts",
|
||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
|
||||
@ -138,6 +145,7 @@
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@google-cloud/kms": "^4.5.0",
|
||||
"@infisical/quic": "^1.0.8",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/auth-app": "^7.1.1",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
@ -145,10 +153,10 @@
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@octopusdeploy/api-client": "^3.4.1",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.53.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.55.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.55.0",
|
||||
"@opentelemetry/instrumentation": "^0.55.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.57.2",
|
||||
"@opentelemetry/resources": "^1.28.0",
|
||||
"@opentelemetry/sdk-metrics": "^1.28.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.27.0",
|
||||
@ -169,6 +177,7 @@
|
||||
"cassandra-driver": "^4.7.2",
|
||||
"connect-redis": "^7.1.1",
|
||||
"cron": "^3.1.7",
|
||||
"dd-trace": "^5.40.0",
|
||||
"dotenv": "^16.4.1",
|
||||
"fastify": "^4.28.1",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
@ -177,6 +186,7 @@
|
||||
"handlebars": "^4.7.8",
|
||||
"hdb": "^0.19.10",
|
||||
"ioredis": "^5.3.2",
|
||||
"isomorphic-dompurify": "^2.22.0",
|
||||
"jmespath": "^0.16.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsrp": "^0.2.4",
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -13,6 +13,7 @@ import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/
|
||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
|
||||
@ -228,6 +229,7 @@ declare module "fastify" {
|
||||
secretSync: TSecretSyncServiceFactory;
|
||||
kmip: TKmipServiceFactory;
|
||||
kmipOperation: TKmipOperationServiceFactory;
|
||||
gateway: TGatewayServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
20
backend/src/@types/knex.d.ts
vendored
20
backend/src/@types/knex.d.ts
vendored
@ -68,6 +68,9 @@ import {
|
||||
TExternalKms,
|
||||
TExternalKmsInsert,
|
||||
TExternalKmsUpdate,
|
||||
TGateways,
|
||||
TGatewaysInsert,
|
||||
TGatewaysUpdate,
|
||||
TGitAppInstallSessions,
|
||||
TGitAppInstallSessionsInsert,
|
||||
TGitAppInstallSessionsUpdate,
|
||||
@ -179,6 +182,9 @@ import {
|
||||
TOrgBots,
|
||||
TOrgBotsInsert,
|
||||
TOrgBotsUpdate,
|
||||
TOrgGatewayConfig,
|
||||
TOrgGatewayConfigInsert,
|
||||
TOrgGatewayConfigUpdate,
|
||||
TOrgMemberships,
|
||||
TOrgMembershipsInsert,
|
||||
TOrgMembershipsUpdate,
|
||||
@ -200,6 +206,9 @@ import {
|
||||
TProjectEnvironments,
|
||||
TProjectEnvironmentsInsert,
|
||||
TProjectEnvironmentsUpdate,
|
||||
TProjectGateways,
|
||||
TProjectGatewaysInsert,
|
||||
TProjectGatewaysUpdate,
|
||||
TProjectKeys,
|
||||
TProjectKeysInsert,
|
||||
TProjectKeysUpdate,
|
||||
@ -930,5 +939,16 @@ declare module "knex/types/tables" {
|
||||
TKmipClientCertificatesInsert,
|
||||
TKmipClientCertificatesUpdate
|
||||
>;
|
||||
[TableName.Gateway]: KnexOriginal.CompositeTableType<TGateways, TGatewaysInsert, TGatewaysUpdate>;
|
||||
[TableName.ProjectGateway]: KnexOriginal.CompositeTableType<
|
||||
TProjectGateways,
|
||||
TProjectGatewaysInsert,
|
||||
TProjectGatewaysUpdate
|
||||
>;
|
||||
[TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType<
|
||||
TOrgGatewayConfig,
|
||||
TOrgGatewayConfigInsert,
|
||||
TOrgGatewayConfigUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ export default {
|
||||
},
|
||||
migrations: {
|
||||
tableName: "infisical_migrations",
|
||||
loadExtensions: [".mjs"]
|
||||
loadExtensions: [".mjs", ".ts"]
|
||||
}
|
||||
},
|
||||
production: {
|
||||
@ -64,7 +64,7 @@ export default {
|
||||
},
|
||||
migrations: {
|
||||
tableName: "infisical_migrations",
|
||||
loadExtensions: [".mjs"]
|
||||
loadExtensions: [".mjs", ".ts"]
|
||||
}
|
||||
}
|
||||
} as Knex.Config;
|
||||
|
115
backend/src/db/migrations/20250212191958_create-gateway.ts
Normal file
115
backend/src/db/migrations/20250212191958_create-gateway.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.OrgGatewayConfig))) {
|
||||
await knex.schema.createTable(TableName.OrgGatewayConfig, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("rootCaKeyAlgorithm").notNullable();
|
||||
|
||||
t.datetime("rootCaIssuedAt").notNullable();
|
||||
t.datetime("rootCaExpiration").notNullable();
|
||||
t.string("rootCaSerialNumber").notNullable();
|
||||
t.binary("encryptedRootCaCertificate").notNullable();
|
||||
t.binary("encryptedRootCaPrivateKey").notNullable();
|
||||
|
||||
t.datetime("clientCaIssuedAt").notNullable();
|
||||
t.datetime("clientCaExpiration").notNullable();
|
||||
t.string("clientCaSerialNumber");
|
||||
t.binary("encryptedClientCaCertificate").notNullable();
|
||||
t.binary("encryptedClientCaPrivateKey").notNullable();
|
||||
|
||||
t.string("clientCertSerialNumber").notNullable();
|
||||
t.string("clientCertKeyAlgorithm").notNullable();
|
||||
t.datetime("clientCertIssuedAt").notNullable();
|
||||
t.datetime("clientCertExpiration").notNullable();
|
||||
t.binary("encryptedClientCertificate").notNullable();
|
||||
t.binary("encryptedClientPrivateKey").notNullable();
|
||||
|
||||
t.datetime("gatewayCaIssuedAt").notNullable();
|
||||
t.datetime("gatewayCaExpiration").notNullable();
|
||||
t.string("gatewayCaSerialNumber").notNullable();
|
||||
t.binary("encryptedGatewayCaCertificate").notNullable();
|
||||
t.binary("encryptedGatewayCaPrivateKey").notNullable();
|
||||
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.unique("orgId");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.OrgGatewayConfig);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.Gateway))) {
|
||||
await knex.schema.createTable(TableName.Gateway, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.string("name").notNullable();
|
||||
t.string("serialNumber").notNullable();
|
||||
t.string("keyAlgorithm").notNullable();
|
||||
t.datetime("issuedAt").notNullable();
|
||||
t.datetime("expiration").notNullable();
|
||||
t.datetime("heartbeat");
|
||||
|
||||
t.binary("relayAddress").notNullable();
|
||||
|
||||
t.uuid("orgGatewayRootCaId").notNullable();
|
||||
t.foreign("orgGatewayRootCaId").references("id").inTable(TableName.OrgGatewayConfig).onDelete("CASCADE");
|
||||
|
||||
t.uuid("identityId").notNullable();
|
||||
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.Gateway);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ProjectGateway))) {
|
||||
await knex.schema.createTable(TableName.ProjectGateway, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
|
||||
t.uuid("gatewayId").notNullable();
|
||||
t.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("CASCADE");
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.ProjectGateway);
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId");
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||
// not setting a foreign constraint so that cascade effects are not triggered
|
||||
if (!doesGatewayColExist) {
|
||||
t.uuid("projectGatewayId");
|
||||
t.foreign("projectGatewayId").references("id").inTable(TableName.ProjectGateway);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId");
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||
if (doesGatewayColExist) t.dropColumn("projectGatewayId");
|
||||
});
|
||||
}
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.ProjectGateway);
|
||||
await dropOnUpdateTrigger(knex, TableName.ProjectGateway);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.Gateway);
|
||||
await dropOnUpdateTrigger(knex, TableName.Gateway);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.OrgGatewayConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.OrgGatewayConfig);
|
||||
}
|
25
backend/src/db/migrations/20250226021631_secret-requests.ts
Normal file
25
backend/src/db/migrations/20250226021631_secret-requests.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
|
||||
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
|
||||
if (!hasSharingTypeColumn) {
|
||||
table.string("type", 32).defaultTo(SecretSharingType.Share).notNullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
|
||||
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
|
||||
if (hasSharingTypeColumn) {
|
||||
table.dropColumn("type");
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent");
|
||||
const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent");
|
||||
if (await knex.schema.hasTable(TableName.SuperAdmin)) {
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
if (!hasAuthConsentContentCol) {
|
||||
t.text("authConsentContent");
|
||||
}
|
||||
if (!hasPageFrameContentCol) {
|
||||
t.text("pageFrameContent");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent");
|
||||
const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent");
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
if (hasAuthConsentContentCol) {
|
||||
t.dropColumn("authConsentContent");
|
||||
}
|
||||
if (hasPageFrameContentCol) {
|
||||
t.dropColumn("pageFrameContent");
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
for await (const tableName of [
|
||||
TableName.SecretV2,
|
||||
TableName.SecretVersionV2,
|
||||
TableName.SecretApprovalRequestSecretV2
|
||||
]) {
|
||||
const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote");
|
||||
|
||||
if (hasReminderNoteCol) {
|
||||
await knex.schema.alterTable(tableName, (t) => {
|
||||
t.string("reminderNote", 1024).alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
for await (const tableName of [
|
||||
TableName.SecretV2,
|
||||
TableName.SecretVersionV2,
|
||||
TableName.SecretApprovalRequestSecretV2
|
||||
]) {
|
||||
const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote");
|
||||
|
||||
if (hasReminderNoteCol) {
|
||||
await knex.schema.alterTable(tableName, (t) => {
|
||||
t.string("reminderNote").alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasProjectDescription = await knex.schema.hasColumn(TableName.SecretFolder, "description");
|
||||
|
||||
if (!hasProjectDescription) {
|
||||
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
|
||||
t.string("description");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasProjectDescription = await knex.schema.hasColumn(TableName.SecretFolder, "description");
|
||||
|
||||
if (hasProjectDescription) {
|
||||
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
|
||||
t.dropColumn("description");
|
||||
});
|
||||
}
|
||||
}
|
@ -26,7 +26,8 @@ export const DynamicSecretsSchema = z.object({
|
||||
statusDetails: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
encryptedInput: zodBuffer
|
||||
encryptedInput: zodBuffer,
|
||||
projectGatewayId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||
|
29
backend/src/db/schemas/gateways.ts
Normal file
29
backend/src/db/schemas/gateways.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const GatewaysSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
serialNumber: z.string(),
|
||||
keyAlgorithm: z.string(),
|
||||
issuedAt: z.date(),
|
||||
expiration: z.date(),
|
||||
heartbeat: z.date().nullable().optional(),
|
||||
relayAddress: zodBuffer,
|
||||
orgGatewayRootCaId: z.string().uuid(),
|
||||
identityId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TGateways = z.infer<typeof GatewaysSchema>;
|
||||
export type TGatewaysInsert = Omit<z.input<typeof GatewaysSchema>, TImmutableDBKeys>;
|
||||
export type TGatewaysUpdate = Partial<Omit<z.input<typeof GatewaysSchema>, TImmutableDBKeys>>;
|
@ -20,6 +20,7 @@ export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
export * from "./external-kms";
|
||||
export * from "./gateways";
|
||||
export * from "./git-app-install-sessions";
|
||||
export * from "./git-app-org";
|
||||
export * from "./group-project-membership-roles";
|
||||
@ -57,6 +58,7 @@ export * from "./ldap-group-maps";
|
||||
export * from "./models";
|
||||
export * from "./oidc-configs";
|
||||
export * from "./org-bots";
|
||||
export * from "./org-gateway-config";
|
||||
export * from "./org-memberships";
|
||||
export * from "./org-roles";
|
||||
export * from "./organizations";
|
||||
@ -65,6 +67,7 @@ export * from "./pki-collection-items";
|
||||
export * from "./pki-collections";
|
||||
export * from "./project-bots";
|
||||
export * from "./project-environments";
|
||||
export * from "./project-gateways";
|
||||
export * from "./project-keys";
|
||||
export * from "./project-memberships";
|
||||
export * from "./project-roles";
|
||||
|
@ -113,6 +113,10 @@ export enum TableName {
|
||||
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
|
||||
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
|
||||
ProjectSplitBackfillIds = "project_split_backfill_ids",
|
||||
// Gateway
|
||||
OrgGatewayConfig = "org_gateway_config",
|
||||
Gateway = "gateways",
|
||||
ProjectGateway = "project_gateways",
|
||||
// junction tables with tags
|
||||
SecretV2JnTag = "secret_v2_tag_junction",
|
||||
JnSecretTag = "secret_tag_junction",
|
||||
|
43
backend/src/db/schemas/org-gateway-config.ts
Normal file
43
backend/src/db/schemas/org-gateway-config.ts
Normal file
@ -0,0 +1,43 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const OrgGatewayConfigSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
rootCaKeyAlgorithm: z.string(),
|
||||
rootCaIssuedAt: z.date(),
|
||||
rootCaExpiration: z.date(),
|
||||
rootCaSerialNumber: z.string(),
|
||||
encryptedRootCaCertificate: zodBuffer,
|
||||
encryptedRootCaPrivateKey: zodBuffer,
|
||||
clientCaIssuedAt: z.date(),
|
||||
clientCaExpiration: z.date(),
|
||||
clientCaSerialNumber: z.string().nullable().optional(),
|
||||
encryptedClientCaCertificate: zodBuffer,
|
||||
encryptedClientCaPrivateKey: zodBuffer,
|
||||
clientCertSerialNumber: z.string(),
|
||||
clientCertKeyAlgorithm: z.string(),
|
||||
clientCertIssuedAt: z.date(),
|
||||
clientCertExpiration: z.date(),
|
||||
encryptedClientCertificate: zodBuffer,
|
||||
encryptedClientPrivateKey: zodBuffer,
|
||||
gatewayCaIssuedAt: z.date(),
|
||||
gatewayCaExpiration: z.date(),
|
||||
gatewayCaSerialNumber: z.string(),
|
||||
encryptedGatewayCaCertificate: zodBuffer,
|
||||
encryptedGatewayCaPrivateKey: zodBuffer,
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TOrgGatewayConfig = z.infer<typeof OrgGatewayConfigSchema>;
|
||||
export type TOrgGatewayConfigInsert = Omit<z.input<typeof OrgGatewayConfigSchema>, TImmutableDBKeys>;
|
||||
export type TOrgGatewayConfigUpdate = Partial<Omit<z.input<typeof OrgGatewayConfigSchema>, TImmutableDBKeys>>;
|
20
backend/src/db/schemas/project-gateways.ts
Normal file
20
backend/src/db/schemas/project-gateways.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ProjectGatewaysSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
gatewayId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TProjectGateways = z.infer<typeof ProjectGatewaysSchema>;
|
||||
export type TProjectGatewaysInsert = Omit<z.input<typeof ProjectGatewaysSchema>, TImmutableDBKeys>;
|
||||
export type TProjectGatewaysUpdate = Partial<Omit<z.input<typeof ProjectGatewaysSchema>, TImmutableDBKeys>>;
|
@ -15,7 +15,8 @@ export const SecretFoldersSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
envId: z.string().uuid(),
|
||||
parentId: z.string().uuid().nullable().optional(),
|
||||
isReserved: z.boolean().default(false).nullable().optional()
|
||||
isReserved: z.boolean().default(false).nullable().optional(),
|
||||
description: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretFolders = z.infer<typeof SecretFoldersSchema>;
|
||||
|
@ -12,6 +12,7 @@ import { TImmutableDBKeys } from "./models";
|
||||
export const SecretSharingSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedValue: z.string().nullable().optional(),
|
||||
type: z.string(),
|
||||
iv: z.string().nullable().optional(),
|
||||
tag: z.string().nullable().optional(),
|
||||
hashedHex: z.string().nullable().optional(),
|
||||
|
@ -23,7 +23,9 @@ export const SuperAdminSchema = z.object({
|
||||
defaultAuthOrgId: z.string().uuid().nullable().optional(),
|
||||
enabledLoginMethods: z.string().array().nullable().optional(),
|
||||
encryptedSlackClientId: zodBuffer.nullable().optional(),
|
||||
encryptedSlackClientSecret: zodBuffer.nullable().optional()
|
||||
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
|
||||
authConsentContent: z.string().nullable().optional(),
|
||||
pageFrameContent: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||
|
265
backend/src/ee/routes/v1/gateway-router.ts
Normal file
265
backend/src/ee/routes/v1/gateway-router.ts
Normal file
@ -0,0 +1,265 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { GatewaysSchema } from "@app/db/schemas";
|
||||
import { isValidIp } from "@app/lib/ip";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const SanitizedGatewaySchema = GatewaysSchema.pick({
|
||||
id: true,
|
||||
identityId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
issuedAt: true,
|
||||
serialNumber: true,
|
||||
heartbeat: true
|
||||
});
|
||||
|
||||
const isValidRelayAddress = (relayAddress: string) => {
|
||||
const [ip, port] = relayAddress.split(":");
|
||||
return isValidIp(ip) && Number(port) <= 65535 && Number(port) >= 40000;
|
||||
};
|
||||
|
||||
export const registerGatewayRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/register-identity",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
turnServerUsername: z.string(),
|
||||
turnServerPassword: z.string(),
|
||||
turnServerRealm: z.string(),
|
||||
turnServerAddress: z.string(),
|
||||
infisicalStaticIp: z.string().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const relayDetails = await server.services.gateway.getGatewayRelayDetails(
|
||||
req.permission.id,
|
||||
req.permission.orgId,
|
||||
req.permission.authMethod
|
||||
);
|
||||
return relayDetails;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/exchange-cert",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
relayAddress: z.string().refine(isValidRelayAddress, { message: "Invalid relay address" })
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
serialNumber: z.string(),
|
||||
privateKey: z.string(),
|
||||
certificate: z.string(),
|
||||
certificateChain: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const gatewayCertificates = await server.services.gateway.exchangeAllocatedRelayAddress({
|
||||
identityOrg: req.permission.orgId,
|
||||
identityId: req.permission.id,
|
||||
relayAddress: req.body.relayAddress,
|
||||
identityOrgAuthMethod: req.permission.authMethod
|
||||
});
|
||||
return gatewayCertificates;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/heartbeat",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
await server.services.gateway.heartbeat({
|
||||
orgPermission: req.permission
|
||||
});
|
||||
return { message: "Successfully registered heartbeat" };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectId: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateways: SanitizedGatewaySchema.extend({
|
||||
identity: z.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
}),
|
||||
projects: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.array()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateways = await server.services.gateway.listGateways({
|
||||
orgPermission: req.permission
|
||||
});
|
||||
return { gateways };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/projects/:projectId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateways: SanitizedGatewaySchema.extend({
|
||||
identity: z.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
}),
|
||||
projectGatewayId: z.string()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateways = await server.services.gateway.getProjectGateways({
|
||||
projectId: req.params.projectId,
|
||||
projectPermission: req.permission
|
||||
});
|
||||
return { gateways };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateway: SanitizedGatewaySchema.extend({
|
||||
identity: z.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateway = await server.services.gateway.getGatewayById({
|
||||
orgPermission: req.permission,
|
||||
id: req.params.id
|
||||
});
|
||||
return { gateway };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
name: slugSchema({ field: "name" }).optional(),
|
||||
projectIds: z.string().array().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateway: SanitizedGatewaySchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateway = await server.services.gateway.updateGatewayById({
|
||||
orgPermission: req.permission,
|
||||
id: req.params.id,
|
||||
name: req.body.name,
|
||||
projectIds: req.body.projectIds
|
||||
});
|
||||
return { gateway };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateway: SanitizedGatewaySchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateway = await server.services.gateway.deleteGatewayById({
|
||||
orgPermission: req.permission,
|
||||
id: req.params.id
|
||||
});
|
||||
return { gateway };
|
||||
}
|
||||
});
|
||||
};
|
@ -7,6 +7,7 @@ import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||
import { registerExternalKmsRouter } from "./external-kms-router";
|
||||
import { registerGatewayRouter } from "./gateway-router";
|
||||
import { registerGroupRouter } from "./group-router";
|
||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||
import { registerKmipRouter } from "./kmip-router";
|
||||
@ -67,6 +68,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
{ prefix: "/dynamic-secrets" }
|
||||
);
|
||||
|
||||
await server.register(registerGatewayRouter, { prefix: "/gateways" });
|
||||
|
||||
await server.register(
|
||||
async (pkiRouter) => {
|
||||
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
|
||||
|
@ -235,7 +235,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
const tagSchema = SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
})
|
||||
.array()
|
||||
|
@ -35,7 +35,6 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
}).array()
|
||||
})
|
||||
|
@ -250,6 +250,7 @@ export enum EventType {
|
||||
UPDATE_APP_CONNECTION = "update-app-connection",
|
||||
DELETE_APP_CONNECTION = "delete-app-connection",
|
||||
CREATE_SHARED_SECRET = "create-shared-secret",
|
||||
CREATE_SECRET_REQUEST = "create-secret-request",
|
||||
DELETE_SHARED_SECRET = "delete-shared-secret",
|
||||
READ_SHARED_SECRET = "read-shared-secret",
|
||||
GET_SECRET_SYNCS = "get-secret-syncs",
|
||||
@ -1141,6 +1142,7 @@ interface CreateFolderEvent {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -2020,6 +2022,15 @@ interface CreateSharedSecretEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretRequestEvent {
|
||||
type: EventType.CREATE_SECRET_REQUEST;
|
||||
metadata: {
|
||||
id: string;
|
||||
accessType: string;
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSharedSecretEvent {
|
||||
type: EventType.DELETE_SHARED_SECRET;
|
||||
metadata: {
|
||||
@ -2470,4 +2481,5 @@ export type Event =
|
||||
| KmipOperationActivateEvent
|
||||
| KmipOperationRevokeEvent
|
||||
| KmipOperationLocateEvent
|
||||
| KmipOperationRegisterEvent;
|
||||
| KmipOperationRegisterEvent
|
||||
| CreateSecretRequestEvent;
|
||||
|
@ -1,20 +1,31 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { getDbConnectionHost } from "@app/lib/knex";
|
||||
|
||||
export const verifyHostInputValidity = (host: string) => {
|
||||
export const verifyHostInputValidity = (host: string, isGateway = false) => {
|
||||
const appCfg = getConfig();
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
// no need for validation when it's dev
|
||||
if (appCfg.NODE_ENV === "development") return;
|
||||
|
||||
if (host === "host.docker.internal") throw new BadRequestError({ message: "Invalid db host" });
|
||||
|
||||
if (
|
||||
appCfg.isCloud &&
|
||||
!isGateway &&
|
||||
// localhost
|
||||
// internal ips
|
||||
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
||||
(host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
|
||||
if (host === "localhost" || host === "127.0.0.1" || dbHost === host) {
|
||||
if (
|
||||
host === "localhost" ||
|
||||
host === "127.0.0.1" ||
|
||||
(dbHost?.length === host.length && crypto.timingSafeEqual(Buffer.from(dbHost || ""), Buffer.from(host)))
|
||||
) {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
};
|
||||
|
@ -16,6 +16,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
|
||||
|
||||
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
|
||||
import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal";
|
||||
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
||||
import {
|
||||
DynamicSecretStatus,
|
||||
@ -44,6 +45,7 @@ type TDynamicSecretServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||
@ -57,7 +59,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
permissionService,
|
||||
dynamicSecretQueueService,
|
||||
projectDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectGatewayDAL
|
||||
}: TDynamicSecretServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
path,
|
||||
@ -108,6 +111,18 @@ export const dynamicSecretServiceFactory = ({
|
||||
const selectedProvider = dynamicSecretProviders[provider.type];
|
||||
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
|
||||
|
||||
let selectedGatewayId: string | null = null;
|
||||
if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) {
|
||||
const projectGatewayId = inputs.projectGatewayId as string;
|
||||
|
||||
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
|
||||
if (!projectGateway)
|
||||
throw new NotFoundError({
|
||||
message: `Project gateway with ${projectGatewayId} not found`
|
||||
});
|
||||
selectedGatewayId = projectGateway.id;
|
||||
}
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
@ -123,7 +138,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
name
|
||||
name,
|
||||
projectGatewayId: selectedGatewayId
|
||||
});
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
@ -195,6 +211,23 @@ export const dynamicSecretServiceFactory = ({
|
||||
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
||||
|
||||
let selectedGatewayId: string | null = null;
|
||||
if (
|
||||
updatedInput &&
|
||||
typeof updatedInput === "object" &&
|
||||
"projectGatewayId" in updatedInput &&
|
||||
updatedInput?.projectGatewayId
|
||||
) {
|
||||
const projectGatewayId = updatedInput.projectGatewayId as string;
|
||||
|
||||
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
|
||||
if (!projectGateway)
|
||||
throw new NotFoundError({
|
||||
message: `Project gateway with ${projectGatewayId} not found`
|
||||
});
|
||||
selectedGatewayId = projectGateway.id;
|
||||
}
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
@ -204,7 +237,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
status: null,
|
||||
statusDetails: null
|
||||
statusDetails: null,
|
||||
projectGatewayId: selectedGatewayId
|
||||
});
|
||||
|
||||
return updatedDynamicCfg;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||
import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
@ -16,8 +17,14 @@ import { SapHanaProvider } from "./sap-hana";
|
||||
import { SqlDatabaseProvider } from "./sql-database";
|
||||
import { TotpProvider } from "./totp";
|
||||
|
||||
export const buildDynamicSecretProviders = (): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||
type TBuildDynamicSecretProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
|
||||
};
|
||||
|
||||
export const buildDynamicSecretProviders = ({
|
||||
gatewayService
|
||||
}: TBuildDynamicSecretProviderDTO): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }),
|
||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
||||
|
@ -103,7 +103,8 @@ export const DynamicSecretSqlDBSchema = z.object({
|
||||
creationStatement: z.string().trim(),
|
||||
revocationStatement: z.string().trim(),
|
||||
renewStatement: z.string().trim().optional(),
|
||||
ca: z.string().optional()
|
||||
ca: z.string().optional(),
|
||||
projectGatewayId: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export const DynamicSecretCassandraSchema = z.object({
|
||||
|
@ -3,8 +3,10 @@ import knex from "knex";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { withGatewayProxy } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
|
||||
|
||||
@ -25,10 +27,14 @@ const generateUsername = (provider: SqlProviders) => {
|
||||
return alphaNumericNanoId(32);
|
||||
};
|
||||
|
||||
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
type TSqlDatabaseProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
|
||||
};
|
||||
|
||||
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId));
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
@ -45,7 +51,6 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
user: providerInputs.username,
|
||||
password: providerInputs.password,
|
||||
ssl,
|
||||
pool: { min: 0, max: 1 },
|
||||
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
|
||||
// https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66
|
||||
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
|
||||
@ -61,61 +66,112 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
return db;
|
||||
};
|
||||
|
||||
const gatewayProxyWrapper = async (
|
||||
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
|
||||
gatewayCallback: (host: string, port: number) => Promise<void>
|
||||
) => {
|
||||
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string);
|
||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||
await withGatewayProxy(
|
||||
async (port) => {
|
||||
await gatewayCallback("localhost", port);
|
||||
},
|
||||
{
|
||||
targetHost: providerInputs.host,
|
||||
targetPort: providerInputs.port,
|
||||
relayHost,
|
||||
relayPort: Number(relayPort),
|
||||
identityId: relayDetails.identityId,
|
||||
orgId: relayDetails.orgId,
|
||||
tlsOptions: {
|
||||
ca: relayDetails.certChain,
|
||||
cert: relayDetails.certificate,
|
||||
key: relayDetails.privateKey.toString()
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
// oracle needs from keyword
|
||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||
let isConnected = false;
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
// oracle needs from keyword
|
||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||
|
||||
const isConnected = await db.raw(testStatement).then(() => true);
|
||||
await db.destroy();
|
||||
isConnected = await db.raw(testStatement).then(() => true);
|
||||
await db.destroy();
|
||||
};
|
||||
|
||||
if (providerInputs.projectGatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername(providerInputs.client);
|
||||
const password = generatePassword(providerInputs.client);
|
||||
const { database } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
try {
|
||||
const { database } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
});
|
||||
await db.destroy();
|
||||
};
|
||||
if (providerInputs.projectGatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const { database } = providerInputs;
|
||||
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
try {
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
await db.destroy();
|
||||
};
|
||||
if (providerInputs.projectGatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
@ -123,28 +179,35 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
if (!providerInputs.renewStatement) return { entityId };
|
||||
|
||||
const db = await $getClient(providerInputs);
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { database } = providerInputs;
|
||||
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { database } = providerInputs;
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||
username: entityId,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||
username: entityId,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
try {
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
};
|
||||
if (providerInputs.projectGatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
|
86
backend/src/ee/services/gateway/gateway-dal.ts
Normal file
86
backend/src/ee/services/gateway/gateway-dal.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import {
|
||||
buildFindFilter,
|
||||
ormify,
|
||||
selectAllTableCols,
|
||||
sqlNestRelationships,
|
||||
TFindFilter,
|
||||
TFindOpt
|
||||
} from "@app/lib/knex";
|
||||
|
||||
export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>;
|
||||
|
||||
export const gatewayDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.Gateway);
|
||||
|
||||
const find = async (filter: TFindFilter<TGateways>, { offset, limit, sort, tx }: TFindOpt<TGateways> = {}) => {
|
||||
try {
|
||||
const query = (tx || db)(TableName.Gateway)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter(filter))
|
||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
|
||||
.leftJoin(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
|
||||
.leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`)
|
||||
.select(selectAllTableCols(TableName.Gateway))
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
||||
db.ref("slug").withSchema(TableName.Project).as("projectSlug"),
|
||||
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||
);
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
return sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (data) => ({
|
||||
...GatewaysSchema.parse(data),
|
||||
identity: { id: data.identityId, name: data.identityName }
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "projectId",
|
||||
label: "projects" as const,
|
||||
mapper: ({ projectId, projectName, projectSlug }) => ({
|
||||
id: projectId,
|
||||
name: projectName,
|
||||
slug: projectSlug
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` });
|
||||
}
|
||||
};
|
||||
|
||||
const findByProjectId = async (projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const query = (tx || db)(TableName.Gateway)
|
||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
|
||||
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
|
||||
.select(selectAllTableCols(TableName.Gateway))
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||
db.ref("id").withSchema(TableName.ProjectGateway).as("projectGatewayId")
|
||||
)
|
||||
.where({ [`${TableName.ProjectGateway}.projectId` as "projectId"]: projectId });
|
||||
|
||||
const docs = await query;
|
||||
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } }));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...orm, find, findByProjectId };
|
||||
};
|
652
backend/src/ee/services/gateway/gateway-service.ts
Normal file
652
backend/src/ee/services/gateway/gateway-service.ts
Normal file
@ -0,0 +1,652 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { pingGatewayAndVerify } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { getTurnCredentials } from "@app/lib/turn/credentials";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
createSerialNumber,
|
||||
keyAlgorithmToAlgCfg
|
||||
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TGatewayDALFactory } from "./gateway-dal";
|
||||
import {
|
||||
TExchangeAllocatedRelayAddressDTO,
|
||||
TGetGatewayByIdDTO,
|
||||
TGetProjectGatewayByIdDTO,
|
||||
THeartBeatDTO,
|
||||
TListGatewaysDTO,
|
||||
TUpdateGatewayByIdDTO
|
||||
} from "./gateway-types";
|
||||
import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal";
|
||||
import { TProjectGatewayDALFactory } from "./project-gateway-dal";
|
||||
|
||||
type TGatewayServiceFactoryDep = {
|
||||
gatewayDAL: TGatewayDALFactory;
|
||||
projectGatewayDAL: TProjectGatewayDALFactory;
|
||||
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry">;
|
||||
};
|
||||
|
||||
export type TGatewayServiceFactory = ReturnType<typeof gatewayServiceFactory>;
|
||||
const TURN_SERVER_CREDENTIALS_SCHEMA = z.object({
|
||||
username: z.string(),
|
||||
password: z.string()
|
||||
});
|
||||
|
||||
export const gatewayServiceFactory = ({
|
||||
gatewayDAL,
|
||||
licenseService,
|
||||
kmsService,
|
||||
permissionService,
|
||||
orgGatewayConfigDAL,
|
||||
keyStore,
|
||||
projectGatewayDAL
|
||||
}: TGatewayServiceFactoryDep) => {
|
||||
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||
// if (!licenseService.onPremFeatures.gateway) {
|
||||
// throw new BadRequestError({
|
||||
// message:
|
||||
// "Gateway handshake failed due to instance plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
|
||||
// });
|
||||
// }
|
||||
const orgLicensePlan = await licenseService.getPlan(orgId);
|
||||
if (!orgLicensePlan.gateway) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
};
|
||||
|
||||
const getGatewayRelayDetails = async (actorId: string, actorOrgId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||
const TURN_CRED_EXPIRY = 10 * 60; // 10 minutes
|
||||
|
||||
const envCfg = getConfig();
|
||||
await $validateOrgAccessToGateway(actorOrgId, actorId, actorAuthMethod);
|
||||
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
if (!envCfg.GATEWAY_RELAY_AUTH_SECRET || !envCfg.GATEWAY_RELAY_ADDRESS || !envCfg.GATEWAY_RELAY_REALM) {
|
||||
throw new BadRequestError({
|
||||
message: "Gateway handshake failed due to missing instance configuration."
|
||||
});
|
||||
}
|
||||
|
||||
let turnServerUsername = "";
|
||||
let turnServerPassword = "";
|
||||
// keep it in redis for 5mins to avoid generating so many credentials
|
||||
const previousCredential = await keyStore.getItem(KeyStorePrefixes.GatewayIdentityCredential(actorId));
|
||||
if (previousCredential) {
|
||||
const el = await TURN_SERVER_CREDENTIALS_SCHEMA.parseAsync(
|
||||
JSON.parse(decryptor({ cipherTextBlob: Buffer.from(previousCredential, "hex") }).toString())
|
||||
);
|
||||
turnServerUsername = el.username;
|
||||
turnServerPassword = el.password;
|
||||
} else {
|
||||
const el = getTurnCredentials(actorId, envCfg.GATEWAY_RELAY_AUTH_SECRET);
|
||||
await keyStore.setItemWithExpiry(
|
||||
KeyStorePrefixes.GatewayIdentityCredential(actorId),
|
||||
TURN_CRED_EXPIRY,
|
||||
encryptor({
|
||||
plainText: Buffer.from(JSON.stringify({ username: el.username, password: el.password }))
|
||||
}).cipherTextBlob.toString("hex")
|
||||
);
|
||||
turnServerUsername = el.username;
|
||||
turnServerPassword = el.password;
|
||||
}
|
||||
|
||||
return {
|
||||
turnServerUsername,
|
||||
turnServerPassword,
|
||||
turnServerRealm: envCfg.GATEWAY_RELAY_REALM,
|
||||
turnServerAddress: envCfg.GATEWAY_RELAY_ADDRESS,
|
||||
infisicalStaticIp: envCfg.GATEWAY_INFISICAL_STATIC_IP_ADDRESS
|
||||
};
|
||||
};
|
||||
|
||||
const exchangeAllocatedRelayAddress = async ({
|
||||
identityId,
|
||||
identityOrg,
|
||||
relayAddress,
|
||||
identityOrgAuthMethod
|
||||
}: TExchangeAllocatedRelayAddressDTO) => {
|
||||
await $validateOrgAccessToGateway(identityOrg, identityId, identityOrgAuthMethod);
|
||||
const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityOrg
|
||||
});
|
||||
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.transaction(async (tx) => {
|
||||
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayRootCaInit(identityOrg)]);
|
||||
const existingGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: identityOrg });
|
||||
if (existingGatewayConfig) return existingGatewayConfig;
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||
// generate root CA
|
||||
const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const rootCaSerialNumber = createSerialNumber();
|
||||
const rootCaSkObj = crypto.KeyObject.from(rootCaKeys.privateKey);
|
||||
const rootCaIssuedAt = new Date();
|
||||
const rootCaKeyAlgorithm = CertKeyAlgorithm.RSA_2048;
|
||||
const rootCaExpiration = new Date(new Date().setFullYear(2045));
|
||||
const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||
name: `O=${identityOrg},CN=Infisical Gateway Root CA`,
|
||||
serialNumber: rootCaSerialNumber,
|
||||
notBefore: rootCaIssuedAt,
|
||||
notAfter: rootCaExpiration,
|
||||
signingAlgorithm: alg,
|
||||
keys: rootCaKeys,
|
||||
extensions: [
|
||||
// eslint-disable-next-line no-bitwise
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
|
||||
await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
// generate client ca
|
||||
const clientCaSerialNumber = createSerialNumber();
|
||||
const clientCaIssuedAt = new Date();
|
||||
const clientCaExpiration = new Date(new Date().setFullYear(2045));
|
||||
const clientCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const clientCaSkObj = crypto.KeyObject.from(clientCaKeys.privateKey);
|
||||
|
||||
const clientCaCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber: clientCaSerialNumber,
|
||||
subject: `O=${identityOrg},CN=Client Intermediate CA`,
|
||||
issuer: rootCaCert.subject,
|
||||
notBefore: clientCaIssuedAt,
|
||||
notAfter: clientCaExpiration,
|
||||
signingKey: rootCaKeys.privateKey,
|
||||
publicKey: clientCaKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
new x509.KeyUsagesExtension(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
x509.KeyUsageFlags.keyCertSign |
|
||||
x509.KeyUsageFlags.cRLSign |
|
||||
x509.KeyUsageFlags.digitalSignature |
|
||||
x509.KeyUsageFlags.keyEncipherment,
|
||||
true
|
||||
),
|
||||
new x509.BasicConstraintsExtension(true, 0, true),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(clientCaKeys.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
const clientKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const clientCertSerialNumber = createSerialNumber();
|
||||
const clientCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber: clientCertSerialNumber,
|
||||
subject: `O=${identityOrg},OU=gateway-client,CN=cloud`,
|
||||
issuer: clientCaCert.subject,
|
||||
notAfter: clientCaExpiration,
|
||||
notBefore: clientCaIssuedAt,
|
||||
signingKey: clientCaKeys.privateKey,
|
||||
publicKey: clientKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(clientCaCert, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(clientKeys.publicKey),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
|
||||
new x509.KeyUsagesExtension(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] |
|
||||
x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] |
|
||||
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
|
||||
true
|
||||
),
|
||||
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true)
|
||||
]
|
||||
});
|
||||
const clientSkObj = crypto.KeyObject.from(clientKeys.privateKey);
|
||||
|
||||
// generate gateway ca
|
||||
const gatewayCaSerialNumber = createSerialNumber();
|
||||
const gatewayCaIssuedAt = new Date();
|
||||
const gatewayCaExpiration = new Date(new Date().setFullYear(2045));
|
||||
const gatewayCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const gatewayCaSkObj = crypto.KeyObject.from(gatewayCaKeys.privateKey);
|
||||
const gatewayCaCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber: gatewayCaSerialNumber,
|
||||
subject: `O=${identityOrg},CN=Gateway CA`,
|
||||
issuer: rootCaCert.subject,
|
||||
notBefore: gatewayCaIssuedAt,
|
||||
notAfter: gatewayCaExpiration,
|
||||
signingKey: rootCaKeys.privateKey,
|
||||
publicKey: gatewayCaKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
new x509.KeyUsagesExtension(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
x509.KeyUsageFlags.keyCertSign |
|
||||
x509.KeyUsageFlags.cRLSign |
|
||||
x509.KeyUsageFlags.digitalSignature |
|
||||
x509.KeyUsageFlags.keyEncipherment,
|
||||
true
|
||||
),
|
||||
new x509.BasicConstraintsExtension(true, 0, true),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(gatewayCaKeys.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
return orgGatewayConfigDAL.create({
|
||||
orgId: identityOrg,
|
||||
rootCaIssuedAt,
|
||||
rootCaExpiration,
|
||||
rootCaSerialNumber,
|
||||
rootCaKeyAlgorithm,
|
||||
encryptedRootCaPrivateKey: orgKmsEncryptor({
|
||||
plainText: rootCaSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
encryptedRootCaCertificate: orgKmsEncryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob,
|
||||
|
||||
clientCaIssuedAt,
|
||||
clientCaExpiration,
|
||||
clientCaSerialNumber,
|
||||
encryptedClientCaPrivateKey: orgKmsEncryptor({
|
||||
plainText: clientCaSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
encryptedClientCaCertificate: orgKmsEncryptor({
|
||||
plainText: Buffer.from(clientCaCert.rawData)
|
||||
}).cipherTextBlob,
|
||||
|
||||
clientCertIssuedAt: clientCaIssuedAt,
|
||||
clientCertExpiration: clientCaExpiration,
|
||||
clientCertKeyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||
clientCertSerialNumber,
|
||||
encryptedClientPrivateKey: orgKmsEncryptor({
|
||||
plainText: clientSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
encryptedClientCertificate: orgKmsEncryptor({
|
||||
plainText: Buffer.from(clientCert.rawData)
|
||||
}).cipherTextBlob,
|
||||
|
||||
gatewayCaIssuedAt,
|
||||
gatewayCaExpiration,
|
||||
gatewayCaSerialNumber,
|
||||
encryptedGatewayCaPrivateKey: orgKmsEncryptor({
|
||||
plainText: gatewayCaSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
encryptedGatewayCaCertificate: orgKmsEncryptor({
|
||||
plainText: Buffer.from(gatewayCaCert.rawData)
|
||||
}).cipherTextBlob
|
||||
});
|
||||
});
|
||||
|
||||
const rootCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||
})
|
||||
);
|
||||
const clientCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedClientCaCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const gatewayCaAlg = keyAlgorithmToAlgCfg(orgGatewayConfig.rootCaKeyAlgorithm as CertKeyAlgorithm);
|
||||
const gatewayCaSkObj = crypto.createPrivateKey({
|
||||
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedGatewayCaPrivateKey }),
|
||||
format: "der",
|
||||
type: "pkcs8"
|
||||
});
|
||||
const gatewayCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const gatewayCaPrivateKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
gatewayCaSkObj.export({ format: "der", type: "pkcs8" }),
|
||||
gatewayCaAlg,
|
||||
true,
|
||||
["sign"]
|
||||
);
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||
const gatewayKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const certIssuedAt = new Date();
|
||||
// then need to periodically init
|
||||
const certExpireAt = new Date(new Date().setMonth(new Date().getMonth() + 1));
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(gatewayCaCert, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(gatewayKeys.publicKey),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
|
||||
new x509.KeyUsagesExtension(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT],
|
||||
true
|
||||
),
|
||||
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true),
|
||||
// san
|
||||
new x509.SubjectAlternativeNameExtension([{ type: "ip", value: relayAddress.split(":")[0] }], false)
|
||||
];
|
||||
|
||||
const serialNumber = createSerialNumber();
|
||||
const privateKey = crypto.KeyObject.from(gatewayKeys.privateKey);
|
||||
const gatewayCertificate = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: `CN=${identityId},O=${identityOrg},OU=Gateway`,
|
||||
issuer: gatewayCaCert.subject,
|
||||
notBefore: certIssuedAt,
|
||||
notAfter: certExpireAt,
|
||||
signingKey: gatewayCaPrivateKey,
|
||||
publicKey: gatewayKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
// just for local development
|
||||
const formatedRelayAddress =
|
||||
appCfg.NODE_ENV === "development" ? relayAddress.replace("127.0.0.1", "host.docker.internal") : relayAddress;
|
||||
|
||||
await gatewayDAL.transaction(async (tx) => {
|
||||
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayCertExchange(identityOrg)]);
|
||||
const existingGateway = await gatewayDAL.findOne({ identityId, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||
|
||||
if (existingGateway) {
|
||||
return gatewayDAL.updateById(existingGateway.id, {
|
||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||
issuedAt: certIssuedAt,
|
||||
expiration: certExpireAt,
|
||||
serialNumber,
|
||||
relayAddress: orgKmsEncryptor({
|
||||
plainText: Buffer.from(formatedRelayAddress)
|
||||
}).cipherTextBlob
|
||||
});
|
||||
}
|
||||
|
||||
return gatewayDAL.create({
|
||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||
issuedAt: certIssuedAt,
|
||||
expiration: certExpireAt,
|
||||
serialNumber,
|
||||
relayAddress: orgKmsEncryptor({
|
||||
plainText: Buffer.from(formatedRelayAddress)
|
||||
}).cipherTextBlob,
|
||||
identityId,
|
||||
orgGatewayRootCaId: orgGatewayConfig.id,
|
||||
name: `gateway-${alphaNumericNanoId(6).toLowerCase()}`
|
||||
});
|
||||
});
|
||||
|
||||
const gatewayCertificateChain = `${clientCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim();
|
||||
|
||||
return {
|
||||
serialNumber,
|
||||
privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) as string,
|
||||
certificate: gatewayCertificate.toString("pem"),
|
||||
certificateChain: gatewayCertificateChain
|
||||
};
|
||||
};
|
||||
|
||||
const heartbeat = async ({ orgPermission }: THeartBeatDTO) => {
|
||||
await $validateOrgAccessToGateway(orgPermission.orgId, orgPermission.id, orgPermission.authMethod);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) throw new NotFoundError({ message: `Identity with ID ${orgPermission.id} not found.` });
|
||||
|
||||
const [gateway] = await gatewayDAL.find({ identityId: orgPermission.id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${orgPermission.id} not found.` });
|
||||
|
||||
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: orgGatewayConfig.orgId
|
||||
});
|
||||
|
||||
const rootCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||
})
|
||||
);
|
||||
const gatewayCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||
})
|
||||
);
|
||||
const clientCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedClientCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const privateKey = crypto
|
||||
.createPrivateKey({
|
||||
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }),
|
||||
format: "der",
|
||||
type: "pkcs8"
|
||||
})
|
||||
.export({ type: "pkcs8", format: "pem" });
|
||||
|
||||
const relayAddress = orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString();
|
||||
const [relayHost, relayPort] = relayAddress.split(":");
|
||||
|
||||
await pingGatewayAndVerify({
|
||||
relayHost,
|
||||
relayPort: Number(relayPort),
|
||||
tlsOptions: {
|
||||
key: privateKey.toString(),
|
||||
ca: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
|
||||
cert: clientCert.toString("pem")
|
||||
},
|
||||
identityId: orgPermission.id,
|
||||
orgId: orgPermission.orgId
|
||||
});
|
||||
|
||||
await gatewayDAL.updateById(gateway.id, { heartbeat: new Date() });
|
||||
};
|
||||
|
||||
const listGateways = async ({ orgPermission }: TListGatewaysDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.ListGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) return [];
|
||||
|
||||
const gateways = await gatewayDAL.find({
|
||||
orgGatewayRootCaId: orgGatewayConfig.id
|
||||
});
|
||||
return gateways;
|
||||
};
|
||||
|
||||
const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.ListGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
|
||||
const [gateway] = await gatewayDAL.find({ id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
return gateway;
|
||||
};
|
||||
|
||||
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.EditGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
|
||||
const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name });
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
if (projectIds) {
|
||||
await projectGatewayDAL.transaction(async (tx) => {
|
||||
await projectGatewayDAL.delete({ gatewayId: gateway.id }, tx);
|
||||
await projectGatewayDAL.insertMany(
|
||||
projectIds.map((el) => ({ gatewayId: gateway.id, projectId: el })),
|
||||
tx
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return gateway;
|
||||
};
|
||||
|
||||
const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.DeleteGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
|
||||
const [gateway] = await gatewayDAL.delete({ id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
return gateway;
|
||||
};
|
||||
|
||||
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => {
|
||||
await permissionService.getProjectPermission({
|
||||
projectId,
|
||||
actor: projectPermission.type,
|
||||
actorId: projectPermission.id,
|
||||
actorOrgId: projectPermission.orgId,
|
||||
actorAuthMethod: projectPermission.authMethod,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
const gateways = await gatewayDAL.findByProjectId(projectId);
|
||||
return gateways;
|
||||
};
|
||||
|
||||
// this has no permission check and used for dynamic secrets directly
|
||||
// assumes permission check is already done
|
||||
const fnGetGatewayClientTls = async (projectGatewayId: string) => {
|
||||
const projectGateway = await projectGatewayDAL.findById(projectGatewayId);
|
||||
if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` });
|
||||
|
||||
const { gatewayId } = projectGateway;
|
||||
const gateway = await gatewayDAL.findById(gatewayId);
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
|
||||
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findById(gateway.orgGatewayRootCaId);
|
||||
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: orgGatewayConfig.orgId
|
||||
});
|
||||
|
||||
const rootCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||
})
|
||||
);
|
||||
const gatewayCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||
})
|
||||
);
|
||||
const clientCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedClientCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const clientSkObj = crypto.createPrivateKey({
|
||||
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }),
|
||||
format: "der",
|
||||
type: "pkcs8"
|
||||
});
|
||||
|
||||
return {
|
||||
relayAddress: orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString(),
|
||||
privateKey: clientSkObj.export({ type: "pkcs8", format: "pem" }),
|
||||
certificate: clientCert.toString("pem"),
|
||||
certChain: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
|
||||
identityId: gateway.identityId,
|
||||
orgId: orgGatewayConfig.orgId
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
getGatewayRelayDetails,
|
||||
exchangeAllocatedRelayAddress,
|
||||
listGateways,
|
||||
getGatewayById,
|
||||
updateGatewayById,
|
||||
deleteGatewayById,
|
||||
getProjectGateways,
|
||||
fnGetGatewayClientTls,
|
||||
heartbeat
|
||||
};
|
||||
};
|
39
backend/src/ee/services/gateway/gateway-types.ts
Normal file
39
backend/src/ee/services/gateway/gateway-types.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
export type TExchangeAllocatedRelayAddressDTO = {
|
||||
identityId: string;
|
||||
identityOrg: string;
|
||||
identityOrgAuthMethod: ActorAuthMethod;
|
||||
relayAddress: string;
|
||||
};
|
||||
|
||||
export type TListGatewaysDTO = {
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type TGetGatewayByIdDTO = {
|
||||
id: string;
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type TUpdateGatewayByIdDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
projectIds?: string[];
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type TDeleteGatewayByIdDTO = {
|
||||
id: string;
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type TGetProjectGatewayByIdDTO = {
|
||||
projectId: string;
|
||||
projectPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type THeartBeatDTO = {
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
10
backend/src/ee/services/gateway/org-gateway-config-dal.ts
Normal file
10
backend/src/ee/services/gateway/org-gateway-config-dal.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TOrgGatewayConfigDALFactory = ReturnType<typeof orgGatewayConfigDALFactory>;
|
||||
|
||||
export const orgGatewayConfigDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.OrgGatewayConfig);
|
||||
return orm;
|
||||
};
|
10
backend/src/ee/services/gateway/project-gateway-dal.ts
Normal file
10
backend/src/ee/services/gateway/project-gateway-dal.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TProjectGatewayDALFactory = ReturnType<typeof projectGatewayDALFactory>;
|
||||
|
||||
export const projectGatewayDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.ProjectGateway);
|
||||
return orm;
|
||||
};
|
@ -51,7 +51,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
pkiEst: false,
|
||||
enforceMfa: false,
|
||||
projectTemplates: false,
|
||||
kmip: false
|
||||
kmip: false,
|
||||
gateway: false
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
@ -69,6 +69,7 @@ export type TFeatureSet = {
|
||||
enforceMfa: boolean;
|
||||
projectTemplates: false;
|
||||
kmip: false;
|
||||
gateway: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@ -32,6 +32,14 @@ export enum OrgPermissionAdminConsoleAction {
|
||||
AccessAllProjects = "access-all-projects"
|
||||
}
|
||||
|
||||
export enum OrgPermissionGatewayActions {
|
||||
// is there a better word for this. This mean can an identity be a gateway
|
||||
CreateGateways = "create-gateways",
|
||||
ListGateways = "list-gateways",
|
||||
EditGateways = "edit-gateways",
|
||||
DeleteGateways = "delete-gateways"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
@ -50,7 +58,8 @@ export enum OrgPermissionSubjects {
|
||||
AuditLogs = "audit-logs",
|
||||
ProjectTemplates = "project-templates",
|
||||
AppConnections = "app-connections",
|
||||
Kmip = "kmip"
|
||||
Kmip = "kmip",
|
||||
Gateway = "gateway"
|
||||
}
|
||||
|
||||
export type AppConnectionSubjectFields = {
|
||||
@ -73,6 +82,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
|
||||
| [
|
||||
OrgPermissionAppConnectionActions,
|
||||
(
|
||||
@ -180,6 +190,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@ -264,6 +280,11 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionAppConnectionActions.Delete, OrgPermissionSubjects.AppConnections);
|
||||
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
||||
|
||||
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
|
||||
@ -300,6 +321,8 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
|
||||
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
||||
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
@ -1294,7 +1294,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretMetadata
|
||||
}) => {
|
||||
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
|
||||
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
|
||||
if (tagIds?.length) commitTagIds[newSecretName ?? secretKey] = tagIds;
|
||||
return {
|
||||
...latestSecretVersions[secretId],
|
||||
secretMetadata,
|
||||
|
@ -6,7 +6,6 @@ export const sanitizedSshCertificate = SshCertificatesSchema.pick({
|
||||
sshCertificateTemplateId: true,
|
||||
serialNumber: true,
|
||||
certType: true,
|
||||
publicKey: true,
|
||||
principals: true,
|
||||
keyId: true,
|
||||
notBefore: true,
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
|
||||
import { Redlock, Settings } from "@app/lib/red-lock";
|
||||
|
||||
export enum PgSqlLock {
|
||||
BootUpMigration = 2023,
|
||||
SuperAdminInit = 2024,
|
||||
KmsRootKeyInit = 2025
|
||||
}
|
||||
export const PgSqlLock = {
|
||||
BootUpMigration: 2023,
|
||||
SuperAdminInit: 2024,
|
||||
KmsRootKeyInit: 2025,
|
||||
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
|
||||
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`)
|
||||
} as const;
|
||||
|
||||
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
||||
|
||||
@ -33,7 +36,8 @@ export const KeyStorePrefixes = {
|
||||
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
|
||||
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
||||
`identity-access-token-status:${identityAccessTokenId}`,
|
||||
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`
|
||||
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`,
|
||||
GatewayIdentityCredential: (identityId: string) => `gateway-credentials:${identityId}`
|
||||
};
|
||||
|
||||
export const KeyStoreTtls = {
|
||||
|
@ -638,7 +638,8 @@ export const FOLDERS = {
|
||||
environment: "The slug of the environment to create the folder in.",
|
||||
name: "The name of the folder to create.",
|
||||
path: "The path of the folder to create.",
|
||||
directory: "The directory of the folder to create. (Deprecated in favor of path)"
|
||||
directory: "The directory of the folder to create. (Deprecated in favor of path)",
|
||||
description: "An optional description label for the folder."
|
||||
},
|
||||
UPDATE: {
|
||||
folderId: "The ID of the folder to update.",
|
||||
@ -647,7 +648,8 @@ export const FOLDERS = {
|
||||
path: "The path of the folder to update.",
|
||||
directory: "The new directory of the folder to update. (Deprecated in favor of path)",
|
||||
projectSlug: "The slug of the project where the folder is located.",
|
||||
workspaceId: "The ID of the project where the folder is located."
|
||||
workspaceId: "The ID of the project where the folder is located.",
|
||||
description: "An optional description label for the folder."
|
||||
},
|
||||
DELETE: {
|
||||
folderIdOrName: "The ID or name of the folder to delete.",
|
||||
|
@ -24,6 +24,7 @@ const databaseReadReplicaSchema = z
|
||||
|
||||
const envSchema = z
|
||||
.object({
|
||||
INFISICAL_PLATFORM_VERSION: zpStr(z.string().optional()),
|
||||
PORT: z.coerce.number().default(IS_PACKAGED ? 8080 : 4000),
|
||||
DISABLE_SECRET_SCANNING: z
|
||||
.enum(["true", "false"])
|
||||
@ -184,6 +185,14 @@ const envSchema = z
|
||||
USE_PG_QUEUE: zodStrBool.default("false"),
|
||||
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false"),
|
||||
|
||||
/* Gateway----------------------------------------------------------------------------- */
|
||||
GATEWAY_INFISICAL_STATIC_IP_ADDRESS: zpStr(z.string().optional()),
|
||||
GATEWAY_RELAY_ADDRESS: zpStr(z.string().optional()),
|
||||
GATEWAY_RELAY_REALM: zpStr(z.string().optional()),
|
||||
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
/* ----------------------------------------------------------------------------- */
|
||||
|
||||
/* App Connections ----------------------------------------------------------------------------- */
|
||||
|
||||
// aws
|
||||
@ -208,6 +217,13 @@ const envSchema = z
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
// datadog
|
||||
SHOULD_USE_DATADOG_TRACER: zodStrBool.default("false"),
|
||||
DATADOG_PROFILING_ENABLED: zodStrBool.default("false"),
|
||||
DATADOG_ENV: zpStr(z.string().optional().default("prod")),
|
||||
DATADOG_SERVICE: zpStr(z.string().optional().default("infisical-core")),
|
||||
DATADOG_HOSTNAME: zpStr(z.string().optional()),
|
||||
|
||||
/* CORS ----------------------------------------------------------------------------- */
|
||||
|
||||
CORS_ALLOWED_ORIGINS: zpStr(
|
||||
|
29
backend/src/lib/crypto/hashtext.ts
Normal file
29
backend/src/lib/crypto/hashtext.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// used for postgres lock
|
||||
// this is something postgres does under the hood
|
||||
// convert any string to a unique number
|
||||
export const hashtext = (text: string) => {
|
||||
// Convert text to UTF8 bytes array for consistent behavior with PostgreSQL
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(text);
|
||||
|
||||
// Implementation of hash_any
|
||||
let result = 0;
|
||||
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
result = ((result << 5) + result) ^ bytes[i];
|
||||
// Keep within 32-bit integer range
|
||||
// eslint-disable-next-line no-bitwise
|
||||
result >>>= 0;
|
||||
}
|
||||
|
||||
// Convert to signed 32-bit integer like PostgreSQL
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return result | 0;
|
||||
};
|
||||
|
||||
export const pgAdvisoryLockHashText = (text: string) => {
|
||||
const hash = hashtext(text);
|
||||
// Ensure positive value within PostgreSQL integer range
|
||||
return Math.abs(hash) % 2 ** 31;
|
||||
};
|
337
backend/src/lib/gateway/index.ts
Normal file
337
backend/src/lib/gateway/index.ts
Normal file
@ -0,0 +1,337 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import crypto from "node:crypto";
|
||||
import net from "node:net";
|
||||
|
||||
import quic from "@infisical/quic";
|
||||
|
||||
import { BadRequestError } from "../errors";
|
||||
import { logger } from "../logger";
|
||||
|
||||
const DEFAULT_MAX_RETRIES = 3;
|
||||
const DEFAULT_RETRY_DELAY = 1000; // 1 second
|
||||
|
||||
const parseSubjectDetails = (data: string) => {
|
||||
const values: Record<string, string> = {};
|
||||
data.split("\n").forEach((el) => {
|
||||
const [key, value] = el.split("=");
|
||||
values[key.trim()] = value.trim();
|
||||
});
|
||||
return values;
|
||||
};
|
||||
|
||||
type TTlsOption = { ca: string; cert: string; key: string };
|
||||
|
||||
const createQuicConnection = async (
|
||||
relayHost: string,
|
||||
relayPort: number,
|
||||
tlsOptions: TTlsOption,
|
||||
identityId: string,
|
||||
orgId: string
|
||||
) => {
|
||||
const client = await quic.QUICClient.createQUICClient({
|
||||
host: relayHost,
|
||||
port: relayPort,
|
||||
config: {
|
||||
ca: tlsOptions.ca,
|
||||
cert: tlsOptions.cert,
|
||||
key: tlsOptions.key,
|
||||
applicationProtos: ["infisical-gateway"],
|
||||
verifyPeer: true,
|
||||
verifyCallback: async (certs) => {
|
||||
if (!certs || certs.length === 0) return quic.native.CryptoError.CertificateRequired;
|
||||
const serverCertificate = new crypto.X509Certificate(Buffer.from(certs[0]));
|
||||
const caCertificate = new crypto.X509Certificate(tlsOptions.ca);
|
||||
const isValidServerCertificate = serverCertificate.checkIssued(caCertificate);
|
||||
if (!isValidServerCertificate) return quic.native.CryptoError.BadCertificate;
|
||||
|
||||
const subjectDetails = parseSubjectDetails(serverCertificate.subject);
|
||||
if (subjectDetails.OU !== "Gateway" || subjectDetails.CN !== identityId || subjectDetails.O !== orgId) {
|
||||
return quic.native.CryptoError.CertificateUnknown;
|
||||
}
|
||||
|
||||
if (new Date() > new Date(serverCertificate.validTo) || new Date() < new Date(serverCertificate.validFrom)) {
|
||||
return quic.native.CryptoError.CertificateExpired;
|
||||
}
|
||||
|
||||
const formatedRelayHost =
|
||||
process.env.NODE_ENV === "development" ? relayHost.replace("host.docker.internal", "127.0.0.1") : relayHost;
|
||||
if (!serverCertificate.checkIP(formatedRelayHost)) return quic.native.CryptoError.BadCertificate;
|
||||
},
|
||||
maxIdleTimeout: 90000,
|
||||
keepAliveIntervalTime: 30000
|
||||
},
|
||||
crypto: {
|
||||
ops: {
|
||||
randomBytes: async (data) => {
|
||||
crypto.getRandomValues(new Uint8Array(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
type TPingGatewayAndVerifyDTO = {
|
||||
relayHost: string;
|
||||
relayPort: number;
|
||||
tlsOptions: TTlsOption;
|
||||
maxRetries?: number;
|
||||
identityId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export const pingGatewayAndVerify = async ({
|
||||
relayHost,
|
||||
relayPort,
|
||||
tlsOptions,
|
||||
maxRetries = DEFAULT_MAX_RETRIES,
|
||||
identityId,
|
||||
orgId
|
||||
}: TPingGatewayAndVerifyDTO) => {
|
||||
let lastError: Error | null = null;
|
||||
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
|
||||
throw new BadRequestError({
|
||||
error: err as Error
|
||||
});
|
||||
});
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
|
||||
try {
|
||||
const stream = quicClient.connection.newStream("bidi");
|
||||
const pingWriter = stream.writable.getWriter();
|
||||
await pingWriter.write(Buffer.from("PING\n"));
|
||||
pingWriter.releaseLock();
|
||||
|
||||
// Read PONG response
|
||||
const reader = stream.readable.getReader();
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
throw new BadRequestError({
|
||||
message: "Gateway closed before receiving PONG"
|
||||
});
|
||||
}
|
||||
|
||||
const response = Buffer.from(value).toString();
|
||||
|
||||
if (response !== "PONG\n" && response !== "PONG") {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to Ping. Unexpected response: ${response}`
|
||||
});
|
||||
}
|
||||
|
||||
reader.releaseLock();
|
||||
return;
|
||||
} catch (err) {
|
||||
lastError = err as Error;
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, DEFAULT_RETRY_DELAY);
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await quicClient.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(lastError);
|
||||
throw new BadRequestError({
|
||||
message: `Failed to ping gateway after ${maxRetries} attempts. Last error: ${lastError?.message}`
|
||||
});
|
||||
};
|
||||
|
||||
interface TProxyServer {
|
||||
server: net.Server;
|
||||
port: number;
|
||||
cleanup: () => Promise<void>;
|
||||
}
|
||||
|
||||
const setupProxyServer = async ({
|
||||
targetPort,
|
||||
targetHost,
|
||||
tlsOptions,
|
||||
relayHost,
|
||||
relayPort,
|
||||
identityId,
|
||||
orgId
|
||||
}: {
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
relayPort: number;
|
||||
relayHost: string;
|
||||
tlsOptions: TTlsOption;
|
||||
identityId: string;
|
||||
orgId: string;
|
||||
}): Promise<TProxyServer> => {
|
||||
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
|
||||
throw new BadRequestError({
|
||||
error: err as Error
|
||||
});
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
server.on("connection", async (clientConn) => {
|
||||
try {
|
||||
clientConn.setKeepAlive(true, 30000); // 30 seconds
|
||||
clientConn.setNoDelay(true);
|
||||
|
||||
const stream = quicClient.connection.newStream("bidi");
|
||||
// Send FORWARD-TCP command
|
||||
const forwardWriter = stream.writable.getWriter();
|
||||
await forwardWriter.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`));
|
||||
forwardWriter.releaseLock();
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
// Set up bidirectional copy
|
||||
const setupCopy = async () => {
|
||||
// Client to QUIC
|
||||
// eslint-disable-next-line
|
||||
(async () => {
|
||||
try {
|
||||
const writer = stream.writable.getWriter();
|
||||
|
||||
// Create a handler for client data
|
||||
clientConn.on("data", async (chunk) => {
|
||||
await writer.write(chunk);
|
||||
});
|
||||
|
||||
// Handle client connection close
|
||||
clientConn.on("end", async () => {
|
||||
await writer.close();
|
||||
});
|
||||
|
||||
clientConn.on("error", async (err) => {
|
||||
await writer.abort(err);
|
||||
});
|
||||
} catch (err) {
|
||||
clientConn.destroy();
|
||||
}
|
||||
})();
|
||||
|
||||
// QUIC to Client
|
||||
void (async () => {
|
||||
try {
|
||||
const reader = stream.readable.getReader();
|
||||
|
||||
let reading = true;
|
||||
while (reading) {
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
reading = false;
|
||||
clientConn.end(); // Close client connection when QUIC stream ends
|
||||
break;
|
||||
}
|
||||
|
||||
// Write data to TCP client
|
||||
const canContinue = clientConn.write(Buffer.from(value));
|
||||
|
||||
// Handle backpressure
|
||||
if (!canContinue) {
|
||||
await new Promise((res) => {
|
||||
clientConn.once("drain", res);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
clientConn.destroy();
|
||||
}
|
||||
})();
|
||||
};
|
||||
await setupCopy();
|
||||
//
|
||||
// Handle connection closure
|
||||
clientConn.on("close", async () => {
|
||||
await stream.destroy();
|
||||
});
|
||||
|
||||
const cleanup = async () => {
|
||||
clientConn?.destroy();
|
||||
await stream.destroy();
|
||||
};
|
||||
|
||||
clientConn.on("error", (err) => {
|
||||
logger.error(err, "Client socket error");
|
||||
void cleanup();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
clientConn.on("end", cleanup);
|
||||
} catch (err) {
|
||||
logger.error(err, "Failed to establish target connection:");
|
||||
clientConn.end();
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
server.on("close", async () => {
|
||||
await quicClient?.destroy();
|
||||
});
|
||||
|
||||
/* eslint-enable */
|
||||
|
||||
server.listen(0, () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close();
|
||||
reject(new Error("Failed to get server port"));
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Gateway proxy started");
|
||||
resolve({
|
||||
server,
|
||||
port: address.port,
|
||||
cleanup: async () => {
|
||||
server.close();
|
||||
await quicClient?.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
interface ProxyOptions {
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
relayHost: string;
|
||||
relayPort: number;
|
||||
tlsOptions: TTlsOption;
|
||||
identityId: string;
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
export const withGatewayProxy = async (
|
||||
callback: (port: number) => Promise<void>,
|
||||
options: ProxyOptions
|
||||
): Promise<void> => {
|
||||
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
|
||||
|
||||
// Setup the proxy server
|
||||
const { port, cleanup } = await setupProxyServer({
|
||||
targetHost,
|
||||
targetPort,
|
||||
relayPort,
|
||||
relayHost,
|
||||
tlsOptions,
|
||||
identityId,
|
||||
orgId
|
||||
});
|
||||
|
||||
try {
|
||||
// Execute the callback with the allocated port
|
||||
await callback(port);
|
||||
} catch (err) {
|
||||
logger.error(err, "Failed to proxy");
|
||||
throw new BadRequestError({ message: (err as Error)?.message });
|
||||
} finally {
|
||||
// Ensure cleanup happens regardless of success or failure
|
||||
await cleanup();
|
||||
}
|
||||
};
|
@ -1,11 +1,12 @@
|
||||
import opentelemetry, { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
||||
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
||||
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
||||
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
|
||||
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
||||
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
|
||||
import { Resource } from "@opentelemetry/resources";
|
||||
import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
||||
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
||||
import tracer from "dd-trace";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
import { initEnvConfig } from "../config/env";
|
||||
@ -69,7 +70,7 @@ const initTelemetryInstrumentation = ({
|
||||
opentelemetry.metrics.setGlobalMeterProvider(meterProvider);
|
||||
|
||||
registerInstrumentations({
|
||||
instrumentations: [getNodeAutoInstrumentations()]
|
||||
instrumentations: [new HttpInstrumentation()]
|
||||
});
|
||||
};
|
||||
|
||||
@ -86,6 +87,17 @@ const setupTelemetry = () => {
|
||||
exportType: appCfg.OTEL_EXPORT_TYPE
|
||||
});
|
||||
}
|
||||
|
||||
if (appCfg.SHOULD_USE_DATADOG_TRACER) {
|
||||
console.log("Initializing Datadog tracer");
|
||||
tracer.init({
|
||||
profiling: appCfg.DATADOG_PROFILING_ENABLED,
|
||||
version: appCfg.INFISICAL_PLATFORM_VERSION,
|
||||
env: appCfg.DATADOG_ENV,
|
||||
service: appCfg.DATADOG_SERVICE,
|
||||
hostname: appCfg.DATADOG_HOSTNAME
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
void setupTelemetry();
|
||||
|
16
backend/src/lib/turn/credentials.ts
Normal file
16
backend/src/lib/turn/credentials.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const TURN_TOKEN_TTL = 60 * 60 * 1000; // 24 hours in milliseconds
|
||||
export const getTurnCredentials = (id: string, authSecret: string, ttl = TURN_TOKEN_TTL) => {
|
||||
const timestamp = Math.floor((Date.now() + ttl) / 1000);
|
||||
const username = `${timestamp}:${id}`;
|
||||
|
||||
const hmac = crypto.createHmac("sha1", authSecret);
|
||||
hmac.update(username);
|
||||
const password = hmac.digest("base64");
|
||||
|
||||
return {
|
||||
username,
|
||||
password
|
||||
};
|
||||
};
|
@ -27,6 +27,10 @@ import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-
|
||||
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal";
|
||||
import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
|
||||
import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal";
|
||||
import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal";
|
||||
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { groupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
@ -393,6 +397,10 @@ export const registerRoutes = async (
|
||||
const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db);
|
||||
const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db);
|
||||
|
||||
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
|
||||
const gatewayDAL = gatewayDALFactory(db);
|
||||
const projectGatewayDAL = projectGatewayDALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
orgRoleDAL,
|
||||
@ -1088,7 +1096,9 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
secretSharingDAL,
|
||||
orgDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
smtpService,
|
||||
userDAL
|
||||
});
|
||||
|
||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||
@ -1300,7 +1310,19 @@ export const registerRoutes = async (
|
||||
kmsService
|
||||
});
|
||||
|
||||
const dynamicSecretProviders = buildDynamicSecretProviders();
|
||||
const gatewayService = gatewayServiceFactory({
|
||||
permissionService,
|
||||
gatewayDAL,
|
||||
kmsService,
|
||||
licenseService,
|
||||
orgGatewayConfigDAL,
|
||||
keyStore,
|
||||
projectGatewayDAL
|
||||
});
|
||||
|
||||
const dynamicSecretProviders = buildDynamicSecretProviders({
|
||||
gatewayService
|
||||
});
|
||||
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
|
||||
queueService,
|
||||
dynamicSecretLeaseDAL,
|
||||
@ -1318,8 +1340,10 @@ export const registerRoutes = async (
|
||||
folderDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectGatewayDAL
|
||||
});
|
||||
|
||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||
projectDAL,
|
||||
permissionService,
|
||||
@ -1557,7 +1581,8 @@ export const registerRoutes = async (
|
||||
appConnection: appConnectionService,
|
||||
secretSync: secretSyncService,
|
||||
kmip: kmipService,
|
||||
kmipOperation: kmipOperationService
|
||||
kmipOperation: kmipOperationService,
|
||||
gateway: gatewayService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
@ -1,3 +1,4 @@
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
||||
@ -72,7 +73,21 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
message: "At least one login method should be enabled."
|
||||
}),
|
||||
slackClientId: z.string().optional(),
|
||||
slackClientSecret: z.string().optional()
|
||||
slackClientSecret: z.string().optional(),
|
||||
authConsentContent: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((content) => DOMPurify.sanitize(content) === content, {
|
||||
message: "Auth consent content contains unsafe HTML."
|
||||
})
|
||||
.optional(),
|
||||
pageFrameContent: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((content) => DOMPurify.sanitize(content) === content, {
|
||||
message: "Page frame content contains unsafe HTML."
|
||||
})
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@ -37,6 +37,7 @@ import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||
import { registerProjectRouter } from "./project-router";
|
||||
import { registerSecretFolderRouter } from "./secret-folder-router";
|
||||
import { registerSecretImportRouter } from "./secret-import-router";
|
||||
import { registerSecretRequestsRouter } from "./secret-requests-router";
|
||||
import { registerSecretSharingRouter } from "./secret-sharing-router";
|
||||
import { registerSecretTagRouter } from "./secret-tag-router";
|
||||
import { registerSlackRouter } from "./slack-router";
|
||||
@ -110,7 +111,15 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
||||
await server.register(registerWebhookRouter, { prefix: "/webhooks" });
|
||||
await server.register(registerIdentityRouter, { prefix: "/identities" });
|
||||
await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
|
||||
|
||||
await server.register(
|
||||
async (secretSharingRouter) => {
|
||||
await secretSharingRouter.register(registerSecretSharingRouter, { prefix: "/shared" });
|
||||
await secretSharingRouter.register(registerSecretRequestsRouter, { prefix: "/requests" });
|
||||
},
|
||||
{ prefix: "/secret-sharing" }
|
||||
);
|
||||
|
||||
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
|
||||
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||
await server.register(registerCmekRouter, { prefix: "/kms" });
|
||||
|
@ -47,7 +47,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.CREATE.directory)
|
||||
.describe(FOLDERS.CREATE.directory),
|
||||
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -65,7 +66,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
projectId: req.body.workspaceId,
|
||||
path
|
||||
path,
|
||||
description: req.body.description
|
||||
});
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
@ -76,7 +78,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
environment: req.body.environment,
|
||||
folderId: folder.id,
|
||||
folderName: folder.name,
|
||||
folderPath: path
|
||||
folderPath: path,
|
||||
...(req.body.description ? { description: req.body.description } : {})
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -125,7 +128,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.UPDATE.directory)
|
||||
.describe(FOLDERS.UPDATE.directory),
|
||||
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -196,7 +200,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.UPDATE.path)
|
||||
.describe(FOLDERS.UPDATE.path),
|
||||
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
|
270
backend/src/server/routes/v1/secret-requests-router.ts
Normal file
270
backend/src/server/routes/v1/secret-requests-router.ts
Normal file
@ -0,0 +1,270 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSharingSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SecretSharingAccessType } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerSecretRequestsRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretRequest: SecretSharingSchema.omit({
|
||||
encryptedSecret: true,
|
||||
tag: true,
|
||||
iv: true,
|
||||
encryptedValue: true
|
||||
}).extend({
|
||||
isSecretValueSet: z.boolean(),
|
||||
requester: z.object({
|
||||
organizationName: z.string(),
|
||||
firstName: z.string().nullish(),
|
||||
lastName: z.string().nullish(),
|
||||
username: z.string()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const secretRequest = await req.server.services.secretSharing.getSecretRequestById({
|
||||
id: req.params.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorAuthMethod: req.permission?.authMethod
|
||||
});
|
||||
|
||||
return { secretRequest };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:id/set-value",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
secretValue: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretRequest: SecretSharingSchema.omit({
|
||||
encryptedSecret: true,
|
||||
tag: true,
|
||||
iv: true,
|
||||
encryptedValue: true
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const secretRequest = await req.server.services.secretSharing.setSecretRequestValue({
|
||||
id: req.params.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
secretValue: req.body.secretValue
|
||||
});
|
||||
|
||||
return { secretRequest };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:id/reveal-value",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretRequest: SecretSharingSchema.omit({
|
||||
encryptedSecret: true,
|
||||
tag: true,
|
||||
iv: true,
|
||||
encryptedValue: true
|
||||
}).extend({
|
||||
secretValue: z.string()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const secretRequest = await req.server.services.secretSharing.revealSecretRequestValue({
|
||||
id: req.params.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
orgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
return { secretRequest };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretRequest: SecretSharingSchema.omit({
|
||||
encryptedSecret: true,
|
||||
tag: true,
|
||||
iv: true,
|
||||
encryptedValue: true
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const secretRequest = await req.server.services.secretSharing.deleteSharedSecretById({
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
sharedSecretId: req.params.id,
|
||||
orgId: req.permission.orgId,
|
||||
actor: req.permission.type,
|
||||
type: SecretSharingType.Request
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretRequestDeleted,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
secretRequestId: req.params.id,
|
||||
organizationId: req.permission.orgId,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
return { secretRequest };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number().min(0).max(100).default(0),
|
||||
limit: z.coerce.number().min(1).max(100).default(25)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secrets: z.array(SecretSharingSchema),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { secrets, totalCount } = await req.server.services.secretSharing.getSharedSecrets({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
type: SecretSharingType.Request,
|
||||
...req.query
|
||||
});
|
||||
|
||||
return {
|
||||
secrets,
|
||||
totalCount
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: z.string().max(50).optional(),
|
||||
expiresAt: z.string(),
|
||||
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const shareRequest = await req.server.services.secretSharing.createSecretRequest({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
orgId: req.permission.orgId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.CREATE_SECRET_REQUEST,
|
||||
metadata: {
|
||||
accessType: req.body.accessType,
|
||||
name: req.body.name,
|
||||
id: shareRequest.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretRequestCreated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
secretRequestId: shareRequest.id,
|
||||
organizationId: req.permission.orgId,
|
||||
secretRequestName: req.body.name,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return { id: shareRequest.id };
|
||||
}
|
||||
});
|
||||
};
|
@ -11,6 +11,7 @@ import {
|
||||
} from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
|
||||
|
||||
export const registerSecretSharingRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -38,6 +39,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
type: SecretSharingType.Share,
|
||||
...req.query
|
||||
});
|
||||
|
||||
@ -211,7 +213,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
sharedSecretId
|
||||
sharedSecretId,
|
||||
type: SecretSharingType.Share
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
|
@ -537,7 +537,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(RAW_SECRETS.CREATE.secretReminderRepeatDays),
|
||||
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.CREATE.secretReminderNote)
|
||||
secretReminderNote: z
|
||||
.string()
|
||||
.max(1024, "Secret reminder note cannot exceed 1024 characters")
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(RAW_SECRETS.CREATE.secretReminderNote)
|
||||
}),
|
||||
response: {
|
||||
200: z.union([
|
||||
@ -640,7 +645,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
|
||||
metadata: z.record(z.string()).optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||
secretReminderNote: z
|
||||
.string()
|
||||
.max(1024, "Secret reminder note cannot exceed 1024 characters")
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||
secretReminderRepeatDays: z
|
||||
.number()
|
||||
.optional()
|
||||
@ -2053,7 +2063,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
|
||||
newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName),
|
||||
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
|
||||
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||
secretReminderNote: z
|
||||
.string()
|
||||
.max(1024, "Secret reminder note cannot exceed 1024 characters")
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
secretReminderRepeatDays: z
|
||||
.number()
|
||||
|
@ -20,7 +20,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||
secretDAL: Pick<TSecretDALFactory, "pruneSecretReminders">;
|
||||
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
||||
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
||||
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
|
||||
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
|
||||
queueService: TQueueServiceFactory;
|
||||
};
|
||||
|
||||
@ -45,6 +45,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||
await identityAccessTokenDAL.removeExpiredTokens();
|
||||
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
||||
await secretSharingDAL.pruneExpiredSharedSecrets();
|
||||
await secretSharingDAL.pruneExpiredSecretRequests();
|
||||
await snapshotDAL.pruneExcessSnapshots();
|
||||
await secretVersionDAL.pruneExcessVersions();
|
||||
await secretVersionV2DAL.pruneExcessVersions();
|
||||
|
@ -50,7 +50,8 @@ export const secretFolderServiceFactory = ({
|
||||
actorOrgId,
|
||||
name,
|
||||
environment,
|
||||
path: secretPath
|
||||
path: secretPath,
|
||||
description
|
||||
}: TCreateFolderDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@ -121,7 +122,10 @@ export const secretFolderServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
const doc = await folderDAL.create({ name, envId: env.id, version: 1, parentId: parentFolderId }, tx);
|
||||
const doc = await folderDAL.create(
|
||||
{ name, envId: env.id, version: 1, parentId: parentFolderId, description },
|
||||
tx
|
||||
);
|
||||
await folderVersionDAL.create(
|
||||
{
|
||||
name: doc.name,
|
||||
@ -170,7 +174,7 @@ export const secretFolderServiceFactory = ({
|
||||
const result = await folderDAL.transaction(async (tx) =>
|
||||
Promise.all(
|
||||
folders.map(async (newFolder) => {
|
||||
const { environment, path: secretPath, id, name } = newFolder;
|
||||
const { environment, path: secretPath, id, name, description } = newFolder;
|
||||
|
||||
const parentFolder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
|
||||
if (!parentFolder) {
|
||||
@ -217,7 +221,7 @@ export const secretFolderServiceFactory = ({
|
||||
|
||||
const [doc] = await folderDAL.update(
|
||||
{ envId: env.id, id: folder.id, parentId: parentFolder.id },
|
||||
{ name },
|
||||
{ name, description },
|
||||
tx
|
||||
);
|
||||
await folderVersionDAL.create(
|
||||
@ -259,7 +263,8 @@ export const secretFolderServiceFactory = ({
|
||||
name,
|
||||
environment,
|
||||
path: secretPath,
|
||||
id
|
||||
id,
|
||||
description
|
||||
}: TUpdateFolderDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
@ -312,7 +317,7 @@ export const secretFolderServiceFactory = ({
|
||||
const newFolder = await folderDAL.transaction(async (tx) => {
|
||||
const [doc] = await folderDAL.update(
|
||||
{ envId: env.id, id: folder.id, parentId: parentFolder.id, isReserved: false },
|
||||
{ name },
|
||||
{ name, description },
|
||||
tx
|
||||
);
|
||||
await folderVersionDAL.create(
|
||||
|
@ -9,6 +9,7 @@ export type TCreateFolderDTO = {
|
||||
environment: string;
|
||||
path: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdateFolderDTO = {
|
||||
@ -16,6 +17,7 @@ export type TUpdateFolderDTO = {
|
||||
path: string;
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdateManyFoldersDTO = {
|
||||
@ -25,6 +27,7 @@ export type TUpdateManyFoldersDTO = {
|
||||
path: string;
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
}[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
|
@ -2,17 +2,61 @@ import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TSecretSharing } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueName } from "@app/queue";
|
||||
|
||||
import { SecretSharingType } from "./secret-sharing-types";
|
||||
|
||||
export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>;
|
||||
|
||||
export const secretSharingDALFactory = (db: TDbClient) => {
|
||||
const sharedSecretOrm = ormify(db, TableName.SecretSharing);
|
||||
|
||||
const countAllUserOrgSharedSecrets = async ({ orgId, userId }: { orgId: string; userId: string }) => {
|
||||
const getSecretRequestById = async (id: string) => {
|
||||
const repDb = db.replicaNode();
|
||||
|
||||
const secretRequest = await repDb(TableName.SecretSharing)
|
||||
.leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.SecretSharing}.orgId`)
|
||||
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretSharing}.userId`)
|
||||
.where(`${TableName.SecretSharing}.id`, id)
|
||||
.where(`${TableName.SecretSharing}.type`, SecretSharingType.Request)
|
||||
.select(
|
||||
repDb.ref("name").withSchema(TableName.Organization).as("orgName"),
|
||||
repDb.ref("firstName").withSchema(TableName.Users).as("requesterFirstName"),
|
||||
repDb.ref("lastName").withSchema(TableName.Users).as("requesterLastName"),
|
||||
repDb.ref("username").withSchema(TableName.Users).as("requesterUsername")
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretSharing))
|
||||
.first();
|
||||
|
||||
if (!secretRequest) {
|
||||
throw new NotFoundError({
|
||||
message: `Secret request with ID '${id}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...secretRequest,
|
||||
requester: {
|
||||
organizationName: secretRequest.orgName,
|
||||
firstName: secretRequest.requesterFirstName,
|
||||
lastName: secretRequest.requesterLastName,
|
||||
username: secretRequest.requesterUsername
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const countAllUserOrgSharedSecrets = async ({
|
||||
orgId,
|
||||
userId,
|
||||
type
|
||||
}: {
|
||||
orgId: string;
|
||||
userId: string;
|
||||
type: SecretSharingType;
|
||||
}) => {
|
||||
try {
|
||||
interface CountResult {
|
||||
count: string;
|
||||
@ -22,6 +66,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
||||
.replicaNode()(TableName.SecretSharing)
|
||||
.where(`${TableName.SecretSharing}.orgId`, orgId)
|
||||
.where(`${TableName.SecretSharing}.userId`, userId)
|
||||
.where(`${TableName.SecretSharing}.type`, type)
|
||||
.count("*")
|
||||
.first();
|
||||
|
||||
@ -38,6 +83,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
||||
const docs = await (tx || db)(TableName.SecretSharing)
|
||||
.where("expiresAt", "<", today)
|
||||
.andWhere("encryptedValue", "<>", "")
|
||||
.andWhere("type", SecretSharingType.Share)
|
||||
.update({
|
||||
encryptedValue: "",
|
||||
tag: "",
|
||||
@ -50,6 +96,26 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const pruneExpiredSecretRequests = async (tx?: Knex) => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired secret requests started`);
|
||||
try {
|
||||
const today = new Date();
|
||||
|
||||
const docs = await (tx || db)(TableName.SecretSharing)
|
||||
.whereNotNull("expiresAt")
|
||||
.andWhere("expiresAt", "<", today)
|
||||
.andWhere("encryptedSecret", null)
|
||||
.andWhere("type", SecretSharingType.Request)
|
||||
.delete();
|
||||
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired secret requests completed`);
|
||||
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "pruneExpiredSecretRequests" });
|
||||
}
|
||||
};
|
||||
|
||||
const findActiveSharedSecrets = async (filters: Partial<TSecretSharing>, tx?: Knex) => {
|
||||
try {
|
||||
const now = new Date();
|
||||
@ -57,6 +123,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
||||
.where(filters)
|
||||
.andWhere("expiresAt", ">", now)
|
||||
.andWhere("encryptedValue", "<>", "")
|
||||
.andWhere("type", SecretSharingType.Share)
|
||||
.select(selectAllTableCols(TableName.SecretSharing))
|
||||
.orderBy("expiresAt", "asc");
|
||||
} catch (error) {
|
||||
@ -86,7 +153,9 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
||||
...sharedSecretOrm,
|
||||
countAllUserOrgSharedSecrets,
|
||||
pruneExpiredSharedSecrets,
|
||||
pruneExpiredSecretRequests,
|
||||
softDeleteById,
|
||||
findActiveSharedSecrets
|
||||
findActiveSharedSecrets,
|
||||
getSecretRequestById
|
||||
};
|
||||
};
|
||||
|
@ -4,26 +4,36 @@ import bcrypt from "bcrypt";
|
||||
|
||||
import { TSecretSharing } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { SecretSharingAccessType } from "@app/lib/types";
|
||||
import { isUuidV4 } from "@app/lib/validator";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
||||
import {
|
||||
SecretSharingType,
|
||||
TCreatePublicSharedSecretDTO,
|
||||
TCreateSecretRequestDTO,
|
||||
TCreateSharedSecretDTO,
|
||||
TDeleteSharedSecretDTO,
|
||||
TGetActiveSharedSecretByIdDTO,
|
||||
TGetSharedSecretsDTO
|
||||
TGetSecretRequestByIdDTO,
|
||||
TGetSharedSecretsDTO,
|
||||
TRevealSecretRequestValueDTO,
|
||||
TSetSecretRequestValueDTO
|
||||
} from "./secret-sharing-types";
|
||||
|
||||
type TSecretSharingServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
secretSharingDAL: TSecretSharingDALFactory;
|
||||
orgDAL: TOrgDALFactory;
|
||||
userDAL: TUserDALFactory;
|
||||
kmsService: TKmsServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
};
|
||||
|
||||
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
|
||||
@ -32,7 +42,9 @@ export const secretSharingServiceFactory = ({
|
||||
permissionService,
|
||||
secretSharingDAL,
|
||||
orgDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
smtpService,
|
||||
userDAL
|
||||
}: TSecretSharingServiceFactoryDep) => {
|
||||
const $validateSharedSecretExpiry = (expiresAt: string) => {
|
||||
if (new Date(expiresAt) < new Date()) {
|
||||
@ -75,7 +87,6 @@ export const secretSharingServiceFactory = ({
|
||||
}
|
||||
|
||||
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||
|
||||
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||
|
||||
const id = crypto.randomBytes(32).toString("hex");
|
||||
@ -88,6 +99,7 @@ export const secretSharingServiceFactory = ({
|
||||
encryptedValue: null,
|
||||
encryptedSecret,
|
||||
name,
|
||||
type: SecretSharingType.Share,
|
||||
password: hashedPassword,
|
||||
expiresAt: new Date(expiresAt),
|
||||
expiresAfterViews,
|
||||
@ -101,6 +113,191 @@ export const secretSharingServiceFactory = ({
|
||||
return { id: idToReturn };
|
||||
};
|
||||
|
||||
const createSecretRequest = async ({
|
||||
actor,
|
||||
accessType,
|
||||
expiresAt,
|
||||
name,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TCreateSecretRequestDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
|
||||
|
||||
$validateSharedSecretExpiry(expiresAt);
|
||||
|
||||
const newSecretRequest = await secretSharingDAL.create({
|
||||
type: SecretSharingType.Request,
|
||||
userId: actorId,
|
||||
orgId,
|
||||
name,
|
||||
encryptedSecret: null,
|
||||
accessType,
|
||||
expiresAt: new Date(expiresAt)
|
||||
});
|
||||
|
||||
return { id: newSecretRequest.id };
|
||||
};
|
||||
|
||||
const revealSecretRequestValue = async ({
|
||||
id,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
orgId,
|
||||
actorAuthMethod
|
||||
}: TRevealSecretRequestValueDTO) => {
|
||||
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
|
||||
|
||||
if (!secretRequest) {
|
||||
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
|
||||
|
||||
if (secretRequest.userId !== actorId || secretRequest.orgId !== orgId) {
|
||||
throw new ForbiddenRequestError({ name: "User does not have permission to access this secret request" });
|
||||
}
|
||||
|
||||
if (!secretRequest.encryptedSecret) {
|
||||
throw new BadRequestError({ message: "Secret request has no value set" });
|
||||
}
|
||||
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
const decryptedSecret = decryptWithRoot(secretRequest.encryptedSecret);
|
||||
|
||||
return { ...secretRequest, secretValue: decryptedSecret.toString() };
|
||||
};
|
||||
|
||||
const getSecretRequestById = async ({
|
||||
id,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TGetSecretRequestByIdDTO) => {
|
||||
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
|
||||
|
||||
if (!secretRequest) {
|
||||
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
|
||||
}
|
||||
|
||||
if (secretRequest.accessType === SecretSharingAccessType.Organization) {
|
||||
if (!secretRequest.orgId) {
|
||||
throw new BadRequestError({ message: "No organization ID present on secret request" });
|
||||
}
|
||||
|
||||
if (!actorOrgId) {
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
secretRequest.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
|
||||
}
|
||||
|
||||
if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: Secret request has expired"
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...secretRequest,
|
||||
isSecretValueSet: Boolean(secretRequest.encryptedSecret)
|
||||
};
|
||||
};
|
||||
|
||||
const setSecretRequestValue = async ({
|
||||
id,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
secretValue
|
||||
}: TSetSecretRequestValueDTO) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
|
||||
|
||||
if (!secretRequest) {
|
||||
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
|
||||
}
|
||||
|
||||
let respondentUsername: string | undefined;
|
||||
|
||||
if (secretRequest.accessType === SecretSharingAccessType.Organization) {
|
||||
if (!secretRequest.orgId) {
|
||||
throw new BadRequestError({ message: "No organization ID present on secret request" });
|
||||
}
|
||||
|
||||
if (!actorOrgId) {
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
secretRequest.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
|
||||
|
||||
const user = await userDAL.findById(actorId);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundError({ message: `User with ID '${actorId}' not found` });
|
||||
}
|
||||
|
||||
respondentUsername = user.username;
|
||||
}
|
||||
|
||||
if (secretRequest.encryptedSecret) {
|
||||
throw new BadRequestError({ message: "Secret request already has a value set" });
|
||||
}
|
||||
|
||||
if (secretValue.length > 10_000) {
|
||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||
}
|
||||
|
||||
if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Access denied: Secret request has expired"
|
||||
});
|
||||
}
|
||||
|
||||
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||
|
||||
const request = await secretSharingDAL.transaction(async (tx) => {
|
||||
const updatedRequest = await secretSharingDAL.updateById(id, { encryptedSecret }, tx);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: [secretRequest.requesterUsername],
|
||||
subjectLine: "Secret Request Completed",
|
||||
substitutions: {
|
||||
name: secretRequest.name,
|
||||
respondentUsername,
|
||||
secretRequestUrl: `${appCfg.SITE_URL}/organization/secret-sharing?selectedTab=request-secret`
|
||||
},
|
||||
template: SmtpTemplates.SecretRequestCompleted
|
||||
});
|
||||
|
||||
return updatedRequest;
|
||||
});
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
const createPublicSharedSecret = async ({
|
||||
password,
|
||||
secretValue,
|
||||
@ -121,6 +318,7 @@ export const secretSharingServiceFactory = ({
|
||||
encryptedValue: null,
|
||||
iv: null,
|
||||
tag: null,
|
||||
type: SecretSharingType.Share,
|
||||
encryptedSecret,
|
||||
password: hashedPassword,
|
||||
expiresAt: new Date(expiresAt),
|
||||
@ -137,7 +335,8 @@ export const secretSharingServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
offset,
|
||||
limit
|
||||
limit,
|
||||
type
|
||||
}: TGetSharedSecretsDTO) => {
|
||||
if (!actorOrgId) throw new ForbiddenRequestError();
|
||||
|
||||
@ -153,14 +352,16 @@ export const secretSharingServiceFactory = ({
|
||||
const secrets = await secretSharingDAL.find(
|
||||
{
|
||||
userId: actorId,
|
||||
orgId: actorOrgId
|
||||
orgId: actorOrgId,
|
||||
type
|
||||
},
|
||||
{ offset, limit, sort: [["createdAt", "desc"]] }
|
||||
);
|
||||
|
||||
const count = await secretSharingDAL.countAllUserOrgSharedSecrets({
|
||||
orgId: actorOrgId,
|
||||
userId: actorId
|
||||
userId: actorId,
|
||||
type
|
||||
});
|
||||
|
||||
return {
|
||||
@ -187,9 +388,11 @@ export const secretSharingServiceFactory = ({
|
||||
const sharedSecret = isUuidV4(sharedSecretId)
|
||||
? await secretSharingDAL.findOne({
|
||||
id: sharedSecretId,
|
||||
type: SecretSharingType.Share,
|
||||
hashedHex
|
||||
})
|
||||
: await secretSharingDAL.findOne({
|
||||
type: SecretSharingType.Share,
|
||||
identifier: Buffer.from(sharedSecretId, "base64url").toString("hex")
|
||||
});
|
||||
|
||||
@ -254,7 +457,7 @@ export const secretSharingServiceFactory = ({
|
||||
secret: {
|
||||
...sharedSecret,
|
||||
...(decryptedSecretValue && {
|
||||
secretValue: Buffer.from(decryptedSecretValue).toString()
|
||||
secretValue: decryptedSecretValue.toString()
|
||||
}),
|
||||
orgName:
|
||||
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
||||
@ -270,11 +473,17 @@ export const secretSharingServiceFactory = ({
|
||||
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" });
|
||||
|
||||
const sharedSecret = isUuidV4(sharedSecretId)
|
||||
? await secretSharingDAL.findById(sharedSecretId)
|
||||
: await secretSharingDAL.findOne({ identifier: sharedSecretId });
|
||||
? await secretSharingDAL.findOne({ id: sharedSecretId, type: deleteSharedSecretInput.type })
|
||||
: await secretSharingDAL.findOne({ identifier: sharedSecretId, type: deleteSharedSecretInput.type });
|
||||
|
||||
if (sharedSecret.orgId && sharedSecret.orgId !== orgId)
|
||||
if (sharedSecret.userId !== actorId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "User does not have permission to delete shared secret"
|
||||
});
|
||||
}
|
||||
if (sharedSecret.orgId && sharedSecret.orgId !== orgId) {
|
||||
throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" });
|
||||
}
|
||||
|
||||
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
|
||||
|
||||
@ -286,6 +495,11 @@ export const secretSharingServiceFactory = ({
|
||||
createPublicSharedSecret,
|
||||
getSharedSecrets,
|
||||
deleteSharedSecretById,
|
||||
getSharedSecretById
|
||||
getSharedSecretById,
|
||||
|
||||
createSecretRequest,
|
||||
getSecretRequestById,
|
||||
setSecretRequestValue,
|
||||
revealSecretRequestValue
|
||||
};
|
||||
};
|
||||
|
@ -1,8 +1,14 @@
|
||||
import { SecretSharingAccessType, TGenericPermission } from "@app/lib/types";
|
||||
import { SecretSharingAccessType, TGenericPermission, TOrgPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
|
||||
export enum SecretSharingType {
|
||||
Share = "share",
|
||||
Request = "request"
|
||||
}
|
||||
|
||||
export type TGetSharedSecretsDTO = {
|
||||
type: SecretSharingType;
|
||||
offset: number;
|
||||
limit: number;
|
||||
} & TGenericPermission;
|
||||
@ -39,6 +45,26 @@ export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & {
|
||||
|
||||
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
||||
|
||||
export type TCreateSecretRequestDTO = {
|
||||
name?: string;
|
||||
accessType: SecretSharingAccessType;
|
||||
expiresAt: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TRevealSecretRequestValueDTO = {
|
||||
id: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TGetSecretRequestByIdDTO = {
|
||||
id: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TSetSecretRequestValueDTO = {
|
||||
id: string;
|
||||
secretValue: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TDeleteSharedSecretDTO = {
|
||||
sharedSecretId: string;
|
||||
type: SecretSharingType;
|
||||
} & TSharedSecretPermission;
|
||||
|
@ -39,7 +39,8 @@ export enum SmtpTemplates {
|
||||
SecretSyncFailed = "secretSyncFailed.handlebars",
|
||||
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
|
||||
ExternalImportFailed = "externalImportFailed.handlebars",
|
||||
ExternalImportStarted = "externalImportStarted.handlebars"
|
||||
ExternalImportStarted = "externalImportStarted.handlebars",
|
||||
SecretRequestCompleted = "secretRequestCompleted.handlebars"
|
||||
}
|
||||
|
||||
export enum SmtpHost {
|
||||
|
@ -0,0 +1,33 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Secret Request Completed</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<h2>A secret has been shared with you</h2>
|
||||
|
||||
{{#if name}}
|
||||
<p>Secret request name: {{name}}</p>
|
||||
{{/if}}
|
||||
{{#if respondentUsername}}
|
||||
<p>Shared by: {{respondentUsername}}</p>
|
||||
{{/if}}
|
||||
|
||||
<br />
|
||||
<br/>
|
||||
|
||||
<p>
|
||||
You can access the secret by clicking the link below.
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{secretRequestUrl}}">Access Secret</a>
|
||||
</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
@ -13,7 +13,9 @@ export enum PostHogEventTypes {
|
||||
IntegrationCreated = "Integration Created",
|
||||
MachineIdentityCreated = "Machine Identity Created",
|
||||
UserOrgInvitation = "User Org Invitation",
|
||||
TelemetryInstanceStats = "Self Hosted Instance Stats"
|
||||
TelemetryInstanceStats = "Self Hosted Instance Stats",
|
||||
SecretRequestCreated = "Secret Request Created",
|
||||
SecretRequestDeleted = "Secret Request Deleted"
|
||||
}
|
||||
|
||||
export type TSecretModifiedEvent = {
|
||||
@ -120,6 +122,23 @@ export type TTelemetryInstanceStatsEvent = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TSecretRequestCreatedEvent = {
|
||||
event: PostHogEventTypes.SecretRequestCreated;
|
||||
properties: {
|
||||
secretRequestId: string;
|
||||
organizationId: string;
|
||||
secretRequestName?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TSecretRequestDeletedEvent = {
|
||||
event: PostHogEventTypes.SecretRequestDeleted;
|
||||
properties: {
|
||||
secretRequestId: string;
|
||||
organizationId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TPostHogEvent = { distinctId: string } & (
|
||||
| TSecretModifiedEvent
|
||||
| TAdminInitEvent
|
||||
@ -130,4 +149,6 @@ export type TPostHogEvent = { distinctId: string } & (
|
||||
| TIntegrationCreatedEvent
|
||||
| TProjectCreateEvent
|
||||
| TTelemetryInstanceStatsEvent
|
||||
| TSecretRequestCreatedEvent
|
||||
| TSecretRequestDeletedEvent
|
||||
);
|
||||
|
3
cli/config/infisical-relay.yaml
Normal file
3
cli/config/infisical-relay.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
public_ip: 127.0.0.1
|
||||
auth_secret: changeThisOnProduction
|
||||
realm: infisical.org
|
35
cli/go.mod
35
cli/go.mod
@ -1,6 +1,8 @@
|
||||
module github.com/Infisical/infisical-merge
|
||||
|
||||
go 1.21
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.5
|
||||
|
||||
require (
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0
|
||||
@ -18,14 +20,17 @@ require (
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/muesli/roff v0.1.0
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9
|
||||
github.com/pion/logging v0.2.3
|
||||
github.com/pion/turn/v4 v4.0.0
|
||||
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
|
||||
github.com/quic-go/quic-go v0.50.0
|
||||
github.com/rs/cors v1.11.0
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/term v0.27.0
|
||||
golang.org/x/crypto v0.35.0
|
||||
golang.org/x/term v0.29.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
@ -56,13 +61,15 @@ require (
|
||||
github.com/dvsekhvalnov/jose2go v1.6.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/errors v0.20.2 // indirect
|
||||
github.com/go-openapi/strfmt v0.21.3 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
|
||||
@ -80,7 +87,12 @@ require (
|
||||
github.com/muesli/mango-pflag v0.1.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
|
||||
github.com/pelletier/go-toml v1.9.3 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.4 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
@ -88,6 +100,7 @@ require (
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
|
||||
go.mongodb.org/mongo-driver v1.10.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
@ -96,17 +109,21 @@ require (
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 // indirect
|
||||
golang.org/x/mod v0.23.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/tools v0.30.0 // indirect
|
||||
google.golang.org/api v0.188.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
|
||||
google.golang.org/grpc v1.64.1 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
64
cli/go.sum
64
cli/go.sum
@ -144,8 +144,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/errors v0.20.2 h1:dxy7PGTqEh94zj2E3h1cUmQQWiM1+aeCROfAr02EmK8=
|
||||
@ -154,6 +154,8 @@ github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtK
|
||||
github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
@ -222,6 +224,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro=
|
||||
github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
@ -342,11 +346,27 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
|
||||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 h1:lL+y4Xv20pVlCGyLzNHRC0I0rIHhIL1lTvHizoS/dU8=
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
|
||||
github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
|
||||
github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -357,6 +377,8 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
|
||||
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a h1:Ey0XWvrg6u6hyIn1Kd/jCCmL+bMv9El81tvuGBbxZGg=
|
||||
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a/go.mod h1:oa2sAs9tGai3VldabTV0eWejt/O4/OOD7azP8GaikqU=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo=
|
||||
github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@ -410,6 +432,8 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
|
||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
@ -447,6 +471,8 @@ go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8p
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@ -458,8 +484,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -470,6 +496,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c=
|
||||
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@ -495,6 +523,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -533,8 +563,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -560,8 +590,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -610,11 +640,11 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -624,8 +654,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@ -683,6 +713,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -797,8 +829,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
@ -544,3 +544,59 @@ func CallUpdateRawSecretsV3(httpClient *resty.Client, request UpdateRawSecretByN
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CallRegisterGatewayIdentityV1(httpClient *resty.Client) (*GetRelayCredentialsResponseV1, error) {
|
||||
var resBody GetRelayCredentialsResponseV1
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&resBody).
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
Post(fmt.Sprintf("%v/v1/gateways/register-identity", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CallRegisterGatewayIdentityV1: Unable to complete api request [err=%w]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return nil, fmt.Errorf("CallRegisterGatewayIdentityV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
|
||||
}
|
||||
|
||||
return &resBody, nil
|
||||
}
|
||||
|
||||
func CallExchangeRelayCertV1(httpClient *resty.Client, request ExchangeRelayCertRequestV1) (*ExchangeRelayCertResponseV1, error) {
|
||||
var resBody ExchangeRelayCertResponseV1
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&resBody).
|
||||
SetBody(request).
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
Post(fmt.Sprintf("%v/v1/gateways/exchange-cert", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CallExchangeRelayCertV1: Unable to complete api request [err=%w]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return nil, fmt.Errorf("CallExchangeRelayCertV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
|
||||
}
|
||||
|
||||
return &resBody, nil
|
||||
}
|
||||
|
||||
func CallGatewayHeartBeatV1(httpClient *resty.Client) error {
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
Post(fmt.Sprintf("%v/v1/gateways/heartbeat", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("CallGatewayHeartBeatV1: Unable to complete api request [err=%w]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return fmt.Errorf("CallGatewayHeartBeatV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -629,3 +629,22 @@ type GetRawSecretV3ByNameResponse struct {
|
||||
} `json:"secret"`
|
||||
ETag string
|
||||
}
|
||||
|
||||
type GetRelayCredentialsResponseV1 struct {
|
||||
TurnServerUsername string `json:"turnServerUsername"`
|
||||
TurnServerPassword string `json:"turnServerPassword"`
|
||||
TurnServerRealm string `json:"turnServerRealm"`
|
||||
TurnServerAddress string `json:"turnServerAddress"`
|
||||
InfisicalStaticIp string `json:"infisicalStaticIp"`
|
||||
}
|
||||
|
||||
type ExchangeRelayCertRequestV1 struct {
|
||||
RelayAddress string `json:"relayAddress"`
|
||||
}
|
||||
|
||||
type ExchangeRelayCertResponseV1 struct {
|
||||
SerialNumber string `json:"serialNumber"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Certificate string `json:"certificate"`
|
||||
CertificateChain string `json:"certificateChain"`
|
||||
}
|
||||
|
151
cli/packages/cmd/gateway.go
Normal file
151
cli/packages/cmd/gateway.go
Normal file
@ -0,0 +1,151 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
// "fmt"
|
||||
|
||||
// "github.com/Infisical/infisical-merge/packages/api"
|
||||
// "github.com/Infisical/infisical-merge/packages/models"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/gateway"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
// "github.com/Infisical/infisical-merge/packages/visualize"
|
||||
// "github.com/rs/zerolog/log"
|
||||
|
||||
// "github.com/go-resty/resty/v2"
|
||||
"github.com/posthog/posthog-go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var gatewayCmd = &cobra.Command{
|
||||
Example: `infisical gateway`,
|
||||
Short: "Used to infisical gateway",
|
||||
Use: "gateway",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
util.HandleError(fmt.Errorf("Token not found"))
|
||||
}
|
||||
|
||||
Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION))
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
sigStopCh := make(chan bool, 1)
|
||||
|
||||
ctx, cancel := context.WithCancel(cmd.Context())
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
<-sigCh
|
||||
close(sigStopCh)
|
||||
cancel()
|
||||
|
||||
// If we get a second signal, force exit
|
||||
<-sigCh
|
||||
log.Warn().Msgf("Force exit triggered")
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
// Main gateway retry loop with proper context handling
|
||||
retryTicker := time.NewTicker(5 * time.Second)
|
||||
defer retryTicker.Stop()
|
||||
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
log.Info().Msg("Shutting down gateway")
|
||||
return
|
||||
}
|
||||
gatewayInstance, err := gateway.NewGateway(token.Token)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
if err = gatewayInstance.ConnectWithRelay(); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
log.Info().Msg("Shutting down gateway")
|
||||
return
|
||||
}
|
||||
|
||||
log.Error().Msgf("Gateway connection error with relay: %s", err)
|
||||
log.Info().Msg("Retrying connection in 5 seconds...")
|
||||
select {
|
||||
case <-retryTicker.C:
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
log.Info().Msg("Shutting down gateway")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = gatewayInstance.Listen(ctx)
|
||||
if ctx.Err() != nil {
|
||||
log.Info().Msg("Gateway shutdown complete")
|
||||
return
|
||||
}
|
||||
log.Error().Msgf("Gateway listen error: %s", err)
|
||||
log.Info().Msg("Retrying connection in 5 seconds...")
|
||||
select {
|
||||
case <-retryTicker.C:
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
log.Info().Msg("Shutting down gateway")
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var gatewayRelayCmd = &cobra.Command{
|
||||
Example: `infisical gateway relay`,
|
||||
Short: "Used to run infisical gateway relay",
|
||||
Use: "relay",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
relayConfigFilePath, err := cmd.Flags().GetString("config")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
if relayConfigFilePath == "" {
|
||||
util.HandleError(fmt.Errorf("Missing config file"))
|
||||
}
|
||||
|
||||
gatewayRelay, err := gateway.NewGatewayRelay(relayConfigFilePath)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to initialize gateway")
|
||||
}
|
||||
err = gatewayRelay.Run()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Failed to start gateway")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
gatewayCmd.SetHelpFunc(func(command *cobra.Command, strings []string) {
|
||||
command.Flags().MarkHidden("domain")
|
||||
command.Parent().HelpFunc()(command, strings)
|
||||
})
|
||||
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
|
||||
|
||||
gatewayRelayCmd.Flags().String("config", "", "Relay config yaml file path")
|
||||
|
||||
gatewayCmd.AddCommand(gatewayRelayCmd)
|
||||
|
||||
rootCmd.AddCommand(gatewayCmd)
|
||||
}
|
141
cli/packages/gateway/connection.go
Normal file
141
cli/packages/gateway/connection.go
Normal file
@ -0,0 +1,141 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func handleConnection(ctx context.Context, quicConn quic.Connection) {
|
||||
log.Info().Msgf("New connection from: %s", quicConn.RemoteAddr().String())
|
||||
// Use WaitGroup to track all streams
|
||||
var wg sync.WaitGroup
|
||||
for {
|
||||
// Accept the first stream, which we'll use for commands
|
||||
stream, err := quicConn.AcceptStream(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Failed to accept QUIC stream: %v", err)
|
||||
break
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(stream quic.Stream) {
|
||||
defer wg.Done()
|
||||
defer stream.Close()
|
||||
|
||||
handleStream(stream, quicConn)
|
||||
}(stream)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
log.Printf("All streams closed for connection: %s", quicConn.RemoteAddr().String())
|
||||
}
|
||||
|
||||
func handleStream(stream quic.Stream, quicConn quic.Connection) {
|
||||
streamID := stream.StreamID()
|
||||
log.Printf("New stream %d from: %s", streamID, quicConn.RemoteAddr().String())
|
||||
|
||||
// Use buffered reader for better handling of fragmented data
|
||||
reader := bufio.NewReader(stream)
|
||||
defer stream.Close()
|
||||
|
||||
for {
|
||||
msg, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return
|
||||
}
|
||||
log.Error().Msgf("Error reading command: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
cmd := bytes.ToUpper(bytes.TrimSpace(bytes.Split(msg, []byte(" "))[0]))
|
||||
args := bytes.TrimSpace(bytes.TrimPrefix(msg, cmd))
|
||||
|
||||
switch string(cmd) {
|
||||
case "FORWARD-TCP":
|
||||
log.Info().Msg("Starting secure connector proxy...")
|
||||
proxyAddress := string(bytes.Split(args, []byte(" "))[0])
|
||||
destTarget, err := net.Dial("tcp", proxyAddress)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to connect to target: %v", err)
|
||||
return
|
||||
}
|
||||
defer destTarget.Close()
|
||||
|
||||
// Handle buffered data
|
||||
buffered := reader.Buffered()
|
||||
if buffered > 0 {
|
||||
bufferedData := make([]byte, buffered)
|
||||
_, err := reader.Read(bufferedData)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Error reading buffered data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = destTarget.Write(bufferedData); err != nil {
|
||||
log.Error().Msgf("Error writing buffered data: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
CopyDataFromQuicToTcp(stream, destTarget)
|
||||
return
|
||||
case "PING":
|
||||
if _, err := stream.Write([]byte("PONG\n")); err != nil {
|
||||
log.Error().Msgf("Error writing PONG response: %v", err)
|
||||
}
|
||||
return
|
||||
default:
|
||||
log.Error().Msgf("Unknown command: %s", string(cmd))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type CloseWrite interface {
|
||||
CloseWrite() error
|
||||
}
|
||||
|
||||
func CopyDataFromQuicToTcp(quicStream quic.Stream, tcpConn net.Conn) {
|
||||
// Create a WaitGroup to wait for both copy operations
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
// Start copying from QUIC stream to TCP
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if _, err := io.Copy(tcpConn, quicStream); err != nil {
|
||||
log.Error().Msgf("Error copying quic->postgres: %v", err)
|
||||
}
|
||||
|
||||
if e, ok := tcpConn.(CloseWrite); ok {
|
||||
log.Debug().Msg("Closing TCP write end")
|
||||
e.CloseWrite()
|
||||
} else {
|
||||
log.Debug().Msg("TCP connection does not support CloseWrite")
|
||||
}
|
||||
}()
|
||||
|
||||
// Start copying from TCP to QUIC stream
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if _, err := io.Copy(quicStream, tcpConn); err != nil {
|
||||
log.Debug().Msgf("Error copying postgres->quic: %v", err)
|
||||
}
|
||||
// Close the write side of the QUIC stream
|
||||
if err := quicStream.Close(); err != nil && !strings.Contains(err.Error(), "close called for canceled stream") {
|
||||
log.Error().Msgf("Error closing QUIC stream write: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for both copies to complete
|
||||
wg.Wait()
|
||||
}
|
325
cli/packages/gateway/gateway.go
Normal file
325
cli/packages/gateway/gateway.go
Normal file
@ -0,0 +1,325 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/pion/logging"
|
||||
"github.com/pion/turn/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
type GatewayConfig struct {
|
||||
TurnServerUsername string
|
||||
TurnServerPassword string
|
||||
TurnServerAddress string
|
||||
InfisicalStaticIp string
|
||||
SerialNumber string
|
||||
PrivateKey string
|
||||
Certificate string
|
||||
CertificateChain string
|
||||
}
|
||||
|
||||
type Gateway struct {
|
||||
httpClient *resty.Client
|
||||
config *GatewayConfig
|
||||
client *turn.Client
|
||||
}
|
||||
|
||||
func NewGateway(identityToken string) (Gateway, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(identityToken)
|
||||
|
||||
return Gateway{
|
||||
httpClient: httpClient,
|
||||
config: &GatewayConfig{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) ConnectWithRelay() error {
|
||||
relayDetails, err := api.CallRegisterGatewayIdentityV1(g.httpClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relayAddress, relayPort := strings.Split(relayDetails.TurnServerAddress, ":")[0], strings.Split(relayDetails.TurnServerAddress, ":")[1]
|
||||
var conn net.Conn
|
||||
|
||||
// Dial TURN Server
|
||||
if relayPort == "5349" {
|
||||
log.Info().Msgf("Provided relay port %s. Using TLS", relayPort)
|
||||
conn, err = tls.Dial("tcp", relayDetails.TurnServerAddress, &tls.Config{
|
||||
ServerName: relayAddress,
|
||||
})
|
||||
} else {
|
||||
log.Info().Msgf("Provided relay port %s. Using non TLS connection.", relayPort)
|
||||
peerAddr, errPeer := net.ResolveTCPAddr("tcp", relayDetails.TurnServerAddress)
|
||||
if errPeer != nil {
|
||||
return fmt.Errorf("Failed to parse turn server address: %w", err)
|
||||
}
|
||||
conn, err = net.DialTCP("tcp", nil, peerAddr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to connect with relay server: %w", err)
|
||||
}
|
||||
|
||||
// Start a new TURN Client and wrap our net.Conn in a STUNConn
|
||||
// This allows us to simulate datagram based communication over a net.Conn
|
||||
cfg := &turn.ClientConfig{
|
||||
STUNServerAddr: relayDetails.TurnServerAddress,
|
||||
TURNServerAddr: relayDetails.TurnServerAddress,
|
||||
Conn: turn.NewSTUNConn(conn),
|
||||
Username: relayDetails.TurnServerUsername,
|
||||
Password: relayDetails.TurnServerPassword,
|
||||
Realm: relayDetails.TurnServerRealm,
|
||||
LoggerFactory: logging.NewDefaultLoggerFactory(),
|
||||
}
|
||||
|
||||
client, err := turn.NewClient(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create relay client: %w", err)
|
||||
}
|
||||
|
||||
g.config = &GatewayConfig{
|
||||
TurnServerUsername: relayDetails.TurnServerUsername,
|
||||
TurnServerPassword: relayDetails.TurnServerPassword,
|
||||
TurnServerAddress: relayDetails.TurnServerAddress,
|
||||
InfisicalStaticIp: relayDetails.InfisicalStaticIp,
|
||||
}
|
||||
// if port not specific allow all port
|
||||
if relayDetails.InfisicalStaticIp != "" && !strings.Contains(relayDetails.InfisicalStaticIp, ":") {
|
||||
g.config.InfisicalStaticIp = g.config.InfisicalStaticIp + ":0"
|
||||
}
|
||||
|
||||
g.client = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gateway) Listen(ctx context.Context) error {
|
||||
defer g.client.Close()
|
||||
err := g.client.Listen()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to listen to relay server: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Msg("Connected with relay")
|
||||
|
||||
// Allocate a relay socket on the TURN server. On success, it
|
||||
// will return a net.PacketConn which represents the remote
|
||||
// socket.
|
||||
relayUdpConnection, err := g.client.Allocate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to allocate relay connection: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Msg(relayUdpConnection.LocalAddr().String())
|
||||
defer func() {
|
||||
if closeErr := relayUdpConnection.Close(); closeErr != nil {
|
||||
log.Error().Msgf("Failed to close connection: %s", closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
gatewayCert, err := api.CallExchangeRelayCertV1(g.httpClient, api.ExchangeRelayCertRequestV1{
|
||||
RelayAddress: relayUdpConnection.LocalAddr().String(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.config.SerialNumber = gatewayCert.SerialNumber
|
||||
g.config.PrivateKey = gatewayCert.PrivateKey
|
||||
g.config.Certificate = gatewayCert.Certificate
|
||||
g.config.CertificateChain = gatewayCert.CertificateChain
|
||||
|
||||
shutdownCh := make(chan bool, 1)
|
||||
|
||||
if g.config.InfisicalStaticIp != "" {
|
||||
log.Info().Msgf("Found static ip from Infisical: %s. Creating permission IP lifecycle", g.config.InfisicalStaticIp)
|
||||
peerAddr, err := net.ResolveUDPAddr("udp", g.config.InfisicalStaticIp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse infisical static ip: %w", err)
|
||||
}
|
||||
err = g.client.CreatePermission(peerAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to set permission: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair([]byte(gatewayCert.Certificate), []byte(gatewayCert.PrivateKey))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse cert: %w", err)
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM([]byte(gatewayCert.CertificateChain))
|
||||
|
||||
// Setup QUIC server
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
NextProtos: []string{"infisical-gateway"},
|
||||
}
|
||||
|
||||
// Setup QUIC listener on the relayConn
|
||||
quicConfig := &quic.Config{
|
||||
EnableDatagrams: true,
|
||||
MaxIdleTimeout: 30 * time.Second,
|
||||
KeepAlivePeriod: 15 * time.Second,
|
||||
}
|
||||
|
||||
quicListener, err := quic.Listen(relayUdpConnection, tlsConfig, quicConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to listen for QUIC: %w", err)
|
||||
}
|
||||
defer quicListener.Close()
|
||||
|
||||
log.Printf("Listener started on %s", quicListener.Addr())
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
log.Info().Msg("Gateway started successfully")
|
||||
g.registerHeartBeat(errCh, shutdownCh)
|
||||
g.registerRelayIsActive(relayUdpConnection.LocalAddr().String(), tlsConfig, quicConfig, errCh, shutdownCh)
|
||||
|
||||
// Create a WaitGroup to track active connections
|
||||
var wg sync.WaitGroup
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-shutdownCh:
|
||||
return
|
||||
default:
|
||||
// Accept new relay connection
|
||||
quicConn, err := quicListener.Accept(context.Background())
|
||||
if err != nil {
|
||||
log.Printf("Failed to accept QUIC connection: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
tlsState := quicConn.ConnectionState().TLS
|
||||
if len(tlsState.PeerCertificates) > 0 {
|
||||
organizationUnit := tlsState.PeerCertificates[0].Subject.OrganizationalUnit
|
||||
commonName := tlsState.PeerCertificates[0].Subject.CommonName
|
||||
if organizationUnit[0] != "gateway-client" || commonName != "cloud" {
|
||||
errMsg := fmt.Sprintf("Client certificate verification failed. Received %s, %s", organizationUnit, commonName)
|
||||
log.Error().Msg(errMsg)
|
||||
quicConn.CloseWithError(1, errMsg)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the connection in a goroutine
|
||||
wg.Add(1)
|
||||
go func(c quic.Connection) {
|
||||
defer wg.Done()
|
||||
defer c.CloseWithError(0, "connection closed")
|
||||
|
||||
// Monitor parent context to close this connection when needed
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.CloseWithError(0, "connection closed") // Force close connection when context is canceled
|
||||
case <-shutdownCh:
|
||||
c.CloseWithError(0, "connection closed") // Force close connection when accepting loop is done
|
||||
}
|
||||
}()
|
||||
|
||||
handleConnection(ctx, c)
|
||||
}(quicConn)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Info().Msg("Shutting down gateway...")
|
||||
case err = <-errCh:
|
||||
}
|
||||
|
||||
// Signal the accept loop to stop
|
||||
close(shutdownCh)
|
||||
|
||||
// Set a timeout for waiting on connections to close
|
||||
waitCh := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(waitCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-waitCh:
|
||||
// All connections closed normally
|
||||
case <-time.After(5 * time.Second):
|
||||
log.Warn().Msg("Timeout waiting for connections to close gracefully")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (g *Gateway) registerHeartBeat(errCh chan error, done chan bool) {
|
||||
ticker := time.NewTicker(30 * time.Minute)
|
||||
|
||||
go func() {
|
||||
time.Sleep(10 * time.Second)
|
||||
log.Info().Msg("Registering first heart beat")
|
||||
err := api.CallGatewayHeartBeatV1(g.httpClient)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to register heartbeat: %s", err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
log.Info().Msg("Registering heart beat")
|
||||
err := api.CallGatewayHeartBeatV1(g.httpClient)
|
||||
errCh <- err
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (g *Gateway) registerRelayIsActive(serverAddr string, tlsConf *tls.Config, quicConf *quic.Config, errCh chan error, done chan bool) {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // 3s handshake timeout
|
||||
defer cancel()
|
||||
conn, err := quic.DialAddr(ctx, serverAddr, tlsConf, quicConf)
|
||||
if conn != nil {
|
||||
conn.CloseWithError(0, "connection closed")
|
||||
}
|
||||
// this error means quic connection is alive
|
||||
if err != nil && !strings.Contains(err.Error(), "tls: failed to verify certificate") {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
187
cli/packages/gateway/relay.go
Normal file
187
cli/packages/gateway/relay.go
Normal file
@ -0,0 +1,187 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/pion/logging"
|
||||
"github.com/pion/turn/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/sys/unix"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
errMissingTlsCert = errors.New("Missing TLS files")
|
||||
)
|
||||
|
||||
type GatewayRelay struct {
|
||||
Config *GatewayRelayConfig
|
||||
}
|
||||
|
||||
type GatewayRelayConfig struct {
|
||||
PublicIP string `yaml:"public_ip"`
|
||||
Port int `yaml:"port"`
|
||||
Realm string `yaml:"realm"`
|
||||
AuthSecret string `yaml:"auth_secret"`
|
||||
RelayMinPort uint16 `yaml:"relay_min_port"`
|
||||
RelayMaxPort uint16 `yaml:"relay_max_port"`
|
||||
TlsCertPath string `yaml:"tls_cert_path"`
|
||||
TlsPrivateKeyPath string `yaml:"tls_private_key_path"`
|
||||
|
||||
tls tls.Certificate
|
||||
isTlsEnabled bool
|
||||
}
|
||||
|
||||
func NewGatewayRelay(configFilePath string) (*GatewayRelay, error) {
|
||||
cfgFile, err := os.ReadFile(configFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cfg GatewayRelayConfig
|
||||
if err := yaml.Unmarshal(cfgFile, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfg.PublicIP == "" {
|
||||
return nil, fmt.Errorf("Missing public ip")
|
||||
}
|
||||
|
||||
if cfg.AuthSecret == "" {
|
||||
return nil, fmt.Errorf("Missing auth secret")
|
||||
}
|
||||
|
||||
if cfg.Realm == "" {
|
||||
cfg.Realm = "infisical.org"
|
||||
}
|
||||
|
||||
if cfg.RelayMinPort == 0 {
|
||||
cfg.RelayMinPort = 49152
|
||||
}
|
||||
|
||||
if cfg.RelayMaxPort == 0 {
|
||||
cfg.RelayMaxPort = 65535
|
||||
}
|
||||
|
||||
if cfg.Port == 0 {
|
||||
cfg.Port = 3478
|
||||
} else if cfg.Port == 5349 {
|
||||
if cfg.TlsCertPath == "" || cfg.TlsPrivateKeyPath == "" {
|
||||
return nil, errMissingTlsCert
|
||||
}
|
||||
|
||||
tlsCertFile, err := os.ReadFile(cfg.TlsCertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsPrivateKeyFile, err := os.ReadFile(cfg.TlsPrivateKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(string(tlsCertFile), string(tlsPrivateKeyFile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.tls = cert
|
||||
cfg.isTlsEnabled = true
|
||||
}
|
||||
|
||||
return &GatewayRelay{
|
||||
Config: &cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *GatewayRelay) Run() error {
|
||||
addr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:"+strconv.Itoa(g.Config.Port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse server address: %s", err)
|
||||
}
|
||||
|
||||
// NewLongTermAuthHandler takes a pion.LeveledLogger. This allows you to intercept messages
|
||||
// and process them yourself.
|
||||
logger := logging.NewDefaultLeveledLoggerForScope("lt-creds", logging.LogLevelTrace, os.Stdout)
|
||||
|
||||
// Create `numThreads` UDP listeners to pass into pion/turn
|
||||
// pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in
|
||||
// this allows us to add logging, storage or modify inbound/outbound traffic
|
||||
// UDP listeners share the same local address:port with setting SO_REUSEPORT and the kernel
|
||||
// will load-balance received packets per the IP 5-tuple
|
||||
listenerConfig := &net.ListenConfig{
|
||||
Control: func(network, address string, conn syscall.RawConn) error { // nolint: revive
|
||||
var operr error
|
||||
if err = conn.Control(func(fd uintptr) {
|
||||
operr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return operr
|
||||
},
|
||||
}
|
||||
|
||||
publicIP := g.Config.PublicIP
|
||||
relayAddressGenerator := &turn.RelayAddressGeneratorPortRange{
|
||||
RelayAddress: net.ParseIP(publicIP), // Claim that we are listening on IP passed by user
|
||||
Address: "0.0.0.0", // But actually be listening on every interface
|
||||
MinPort: g.Config.RelayMinPort,
|
||||
MaxPort: g.Config.RelayMaxPort,
|
||||
}
|
||||
|
||||
threadNum := runtime.NumCPU()
|
||||
listenerConfigs := make([]turn.ListenerConfig, threadNum)
|
||||
var connAddress string
|
||||
for i := 0; i < threadNum; i++ {
|
||||
conn, listErr := listenerConfig.Listen(context.Background(), addr.Network(), addr.String())
|
||||
if listErr != nil {
|
||||
return fmt.Errorf("Failed to allocate TCP listener at %s:%s %s", addr.Network(), addr.String(), listErr)
|
||||
}
|
||||
|
||||
listenerConfigs[i] = turn.ListenerConfig{
|
||||
RelayAddressGenerator: relayAddressGenerator,
|
||||
}
|
||||
|
||||
if g.Config.isTlsEnabled {
|
||||
listenerConfigs[i].Listener = tls.NewListener(conn, &tls.Config{
|
||||
Certificates: []tls.Certificate{g.Config.tls},
|
||||
})
|
||||
} else {
|
||||
listenerConfigs[i].Listener = conn
|
||||
}
|
||||
connAddress = conn.Addr().String()
|
||||
}
|
||||
|
||||
loggerF := logging.NewDefaultLoggerFactory()
|
||||
loggerF.DefaultLogLevel = logging.LogLevelDebug
|
||||
|
||||
server, err := turn.NewServer(turn.ServerConfig{
|
||||
Realm: g.Config.Realm,
|
||||
AuthHandler: turn.LongTermTURNRESTAuthHandler(g.Config.AuthSecret, logger),
|
||||
// PacketConnConfigs is a list of UDP Listeners and the configuration around them
|
||||
ListenerConfigs: listenerConfigs,
|
||||
LoggerFactory: loggerF,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to start server: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Msgf("Relay listening on %s\n", connAddress)
|
||||
// Block until user sends SIGINT or SIGTERM
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigs
|
||||
|
||||
if err = server.Close(); err != nil {
|
||||
return fmt.Errorf("Failed to close server: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -12,18 +12,15 @@ Plus, our team is remote-first and spread across the globe (from San Francisco t
|
||||
|
||||
## Onboarding buddy
|
||||
|
||||
Every new joiner has an onboarding buddy who should ideally be in the the same timezone. The onboarding buddy should be able to help with any questions that pop up during the first few weeks. Of course, everyone is available to help, but it's good to have a dedicated person that you can go to with any questions.
|
||||
Every new joiner at Infisical will have an onboarding buddy—a teammate in a similar time zone who’s there to help you settle in. They are your go-to person for any questions that come up. Of course, everyone on the team is happy to help, but it’s always nice to have a dedicated person who’s there for you. Don’t hesitate to reach out to your buddy if you’re unsure about something or need a hand! Your onboarding buddy will set up regular syncs for your first two months—ideally at least 2-3 times a week.
|
||||
|
||||
If you’re joining the engineering team, your onboarding buddy will:
|
||||
1. Walk you through Infisical’s development process and share any best practices to keep in mind when tackling tickets.
|
||||
2. Be your go-to person if you are blocked or need to think through your sprint task.
|
||||
3. Help you ship something small on day one!
|
||||
|
||||
## Onboarding Checklist
|
||||
|
||||
1. Join the weekly all-hands meeting. It typically happens on Monday's at 8:30am PT.
|
||||
2. Ship something together on day one – even if tiny! It feels great to hit the ground running, with a development environment all ready to go.
|
||||
3. Check out the [Areas of Responsibility (AoR) Table](https://docs.google.com/spreadsheets/d/1RnXlGFg83Sgu0dh7ycuydsSobmFfI3A0XkGw7vrVxEI/edit?usp=sharing). This is helpful to know who you can ask about particular areas of Infisical. Feel free to add yourself to the areas you'd be most interesting to dive into.
|
||||
4. Read the [Infisical Strategy Doc](https://docs.google.com/document/d/1uV9IaahYwbZ5OuzDTFdQMSa1P0mpMOnetGB-xqf4G40).
|
||||
5. Update your LinkedIn profile with one of [Infisical's official banners](https://drive.google.com/drive/u/0/folders/1oSNWjbpRl9oNYwxM_98IqzKs9fAskrb2) (if you want to). You can also coordinate your social posts in the #marketing Slack channel, so that we can boost it from Infisical's official social media accounts.
|
||||
6. Over the first few weeks, feel free to schedule 1:1s with folks on the team to get to know them a bit better.
|
||||
7. Change your Slack username in the users channel to `[NAME] (Infisical)`.
|
||||
8. Go through the [technical overview](https://infisical.com/docs/internals/overview) of Infisical.
|
||||
9. Request a company credit card (Maidul will be able to help with that).
|
||||
Your hiring manager will send you an onboarding checklist doc for your first day.
|
||||
|
||||
|
||||
|
@ -24,6 +24,9 @@ Make sure you keep copies for all receipts. If you expense something on a compan
|
||||
|
||||
You should default to using your company card in all cases - it has no transaction fees. If using your personal card is unavoidable, please reach out to Maidul to get it reimbursed manually.
|
||||
|
||||
## Training
|
||||
|
||||
For engineers, you’re welcome to take an approved Udemy course. Please reach out to Maidul. For the GTM team, you may buy a book a month if it’s relevant to your work.
|
||||
|
||||
# Equipment
|
||||
|
||||
@ -55,4 +58,4 @@ For any equipment related questions, please reach out to Maidul.
|
||||
|
||||
## Brex
|
||||
|
||||
We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul.
|
||||
We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul.
|
||||
|
@ -56,4 +56,3 @@ volumes:
|
||||
|
||||
networks:
|
||||
infisical:
|
||||
|
||||
|
@ -11,13 +11,6 @@ To interact with the Infisical API, you will need to obtain an access token. Fol
|
||||
**FAQ**
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="What happened to the Service Token and API Key authentication modes?">
|
||||
The Service Token and API Key authentication modes are being deprecated out in favor of [Identities](/documentation/platform/identity).
|
||||
We expect to make a deprecation notice in the coming months alongside a larger deprecation initiative planned for Q1/Q2 2024.
|
||||
|
||||
With identities, we're improving significantly over the shortcomings of Service Tokens and API Keys. Amongst many differences, identities provide broader access over the Infisical API, utilizes the same role-based
|
||||
permission system used by users, and comes with ample more configurable security measures.
|
||||
</Accordion>
|
||||
<Accordion title="Why can I not create, read, update, or delete an identity?">
|
||||
There are a few reasons for why this might happen:
|
||||
|
||||
|
@ -7,21 +7,22 @@ The Server Admin Console provides **server administrators** with the ability to
|
||||
customize settings and manage users for their entire Infisical instance.
|
||||
|
||||
<Note>
|
||||
The first user to setup an account on your Infisical instance is designated as the server administrator by default.
|
||||
The first user to setup an account on your Infisical instance is designated as
|
||||
the server administrator by default.
|
||||
</Note>
|
||||
|
||||
## Accessing the Server Admin Console
|
||||
|
||||
|
||||
On the sidebar, tap on your initials to access the settings dropdown and press the **Server Admin Console** option.
|
||||
|
||||

|
||||
|
||||
## General Tab
|
||||
|
||||
Configure general settings for your instance.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Allow User Signups
|
||||
|
||||
@ -39,6 +40,22 @@ If you're using SAML/LDAP/OIDC for only one organization on your instance, you c
|
||||
|
||||
By default, users signing up through SAML/LDAP/OIDC will still need to verify their email address to prevent email spoofing. This requirement can be skipped by enabling the switch to trust logins through the respective method.
|
||||
|
||||
### Notices
|
||||
|
||||
Auth consent content is displayed to users on the login page. They can be used to display important information to users, such as a maintenance message or a new feature announcement. Both HTML and Markdown formatting are supported, allowing for customized styling like below:
|
||||
|
||||
```
|
||||
**You are entering a confidential website**
|
||||
```
|
||||
|
||||
```html
|
||||
<div style="font-weight: bold;">You are entering a confidential website</div>
|
||||
```
|
||||
|
||||

|
||||
|
||||
Page frame content is displayed as a header and footer in ALL protected pages. Like the auth consent content, both HTML and Markdown formatting are supported here as well.
|
||||

|
||||
|
||||
## Authentication Tab
|
||||
|
||||
@ -46,16 +63,15 @@ From this tab, you can configure which login methods are enabled for your instan
|
||||
|
||||

|
||||
|
||||
|
||||
## Rate Limit Tab
|
||||
|
||||
This tab allows you to set various rate limits for your Infisical instance. You do not need to redeploy when making changes to rate limits as these will be propagated automatically.
|
||||
|
||||

|
||||
|
||||
|
||||
<Note>
|
||||
Note that rate limit configuration is a paid feature. Please contact sales@infisical.com to purchase a license for its use.
|
||||
Note that rate limit configuration is a paid feature. Please contact
|
||||
sales@infisical.com to purchase a license for its use.
|
||||
</Note>
|
||||
|
||||
## User Management Tab
|
||||
@ -65,5 +81,6 @@ From this tab, you can view all the users who have signed up for your instance.
|
||||

|
||||
|
||||
<Note>
|
||||
Note that rate limit configuration is a paid feature. Please contact sales@infisical.com to purchase a license for its use.
|
||||
Note that rate limit configuration is a paid feature. Please contact
|
||||
sales@infisical.com to purchase a license for its use.
|
||||
</Note>
|
||||
|
81
docs/documentation/platform/gateways/overview.mdx
Normal file
81
docs/documentation/platform/gateways/overview.mdx
Normal file
@ -0,0 +1,81 @@
|
||||
---
|
||||
title: "Gateway"
|
||||
sidebarTitle: "Overview"
|
||||
description: "How to access private network resources from Infisical"
|
||||
---
|
||||
|
||||
The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment.
|
||||
This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases.
|
||||
Common use cases include generating dynamic credentials or rotating credentials for private databases.
|
||||
|
||||
<Info>
|
||||
**Note:** Gateway is a paid feature. - **Infisical Cloud users:** Gateway is
|
||||
available under the **Enterprise Tier**. - **Self-Hosted Infisical:** Please
|
||||
contact [sales@infisical.com](mailto:sales@infisical.com) to purchase an
|
||||
enterprise license.
|
||||
</Info>
|
||||
|
||||
## How It Works
|
||||
|
||||
The Gateway serves as a secure intermediary that facilitates direct communication between the Infisical server and your private network.
|
||||
It’s a lightweight daemon packaged within the Infisical CLI, making it easy to deploy and manage. Once set up, the Gateway establishes a connection with a relay server, ensuring that all communication between Infisical and your Gateway is fully end-to-end encrypted.
|
||||
This setup guarantees that only the platform and your Gateway can decrypt the transmitted information, keeping communication with your resources secure, private and isolated.
|
||||
|
||||
## Deployment
|
||||
|
||||
The Infisical Gateway is seamlessly integrated into the Infisical CLI under the `gateway` command, making it simple to deploy and manage.
|
||||
You can install the Gateway in all the same ways you install the Infisical CLI—whether via npm, Docker, or a binary.
|
||||
For detailed installation instructions, refer to the Infisical [CLI Installation instructions](/cli/overview).
|
||||
|
||||
To function, the Gateway must authenticate with Infisical. This requires a machine identity configured with the appropriate permissions to create and manage a Gateway.
|
||||
Once authenticated, the Gateway establishes a secure connection with Infisical to allow your private resources to be reachable.
|
||||
|
||||
### Deployment process
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a Gateway Identity">
|
||||
1. Navigate to **Organization Access Control** in your Infisical dashboard.
|
||||
2. Create a dedicated machine identity for your Gateway.
|
||||
3. **Best Practice:** Assign a unique identity to each Gateway for better security and management.
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Configure Authentication Method">
|
||||
You'll need to choose an authentication method to initiate communication with Infisical. View the available machine identity authentication methods [here](/documentation/platform/identities/machine-identities).
|
||||
</Step>
|
||||
|
||||
<Step title="Deploy the Gateway">
|
||||
Use the Infisical CLI to deploy the Gateway. You can log in with your machine identity and start the Gateway in one command. The example below demonstrates how to deploy the Gateway using the Universal Auth method:
|
||||
```bash
|
||||
infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain)
|
||||
```
|
||||
Alternatively, if you already have the token, use it directly with the `--token` flag:
|
||||
```bash
|
||||
infisical gateway --token <your-machine-identity-token>
|
||||
```
|
||||
Or set it as an environment variable:
|
||||
```bash
|
||||
export INFISICAL_TOKEN=<your-machine-identity-token>
|
||||
infisical gateway
|
||||
```
|
||||
<Note>
|
||||
Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical.
|
||||
</Note>
|
||||
</Step>
|
||||
|
||||
<Step title="Verify Gateway Deployment">
|
||||
To confirm your Gateway is working, check the deployment status by looking for the message **"Gateway started successfully"** in the Gateway logs. This indicates the Gateway is running properly. Next, verify its registration by opening your Infisical dashboard, navigating to **Organization Access Control**, and selecting the **Gateways** tab. Your newly deployed Gateway should appear in the list.
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Link Gateway to Projects">
|
||||
To enable Infisical features like dynamic secrets or secret rotation to access private resources through the Gateway, you need to link the Gateway to the relevant projects.
|
||||
|
||||
Start by accessing the **Gateway settings** then locate the Gateway in the list, click the options menu (**:**), and select **Edit Details**.
|
||||

|
||||
In the edit modal that appears, choose the projects you want the Gateway to access and click **Save** to confirm your selections.
|
||||

|
||||
Once added to a project, the Gateway becomes available for use by any feature that supports Gateways within that project.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
BIN
docs/images/platform/admin-panels/admin-panel-general-1.png
Normal file
BIN
docs/images/platform/admin-panels/admin-panel-general-1.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 488 KiB |
BIN
docs/images/platform/admin-panels/auth-consent-usage.png
Normal file
BIN
docs/images/platform/admin-panels/auth-consent-usage.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 393 KiB |
BIN
docs/images/platform/admin-panels/page-frame-usage.png
Normal file
BIN
docs/images/platform/admin-panels/page-frame-usage.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 365 KiB |
BIN
docs/images/platform/gateways/assign-project.png
Normal file
BIN
docs/images/platform/gateways/assign-project.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 338 KiB |
BIN
docs/images/platform/gateways/create-identity-for-gateway.png
Normal file
BIN
docs/images/platform/gateways/create-identity-for-gateway.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 375 KiB |
BIN
docs/images/platform/gateways/dynamic-secret.png
Normal file
BIN
docs/images/platform/gateways/dynamic-secret.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 384 KiB |
BIN
docs/images/platform/gateways/edit-gateway.png
Normal file
BIN
docs/images/platform/gateways/edit-gateway.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 365 KiB |
BIN
docs/images/platform/gateways/gateway-list.png
Normal file
BIN
docs/images/platform/gateways/gateway-list.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 433 KiB |
@ -3,18 +3,21 @@ sidebarTitle: "InfisicalDynamicSecret CRD"
|
||||
title: "Using the InfisicalDynamicSecret CRD"
|
||||
description: "Learn how to generate dynamic secret leases in Infisical and sync them to your Kubernetes cluster."
|
||||
---
|
||||
## Overview
|
||||
|
||||
The **InfisicalDynamicSecret** CRD allows you to easily create and manage dynamic secret leases in Infisical and automatically sync them to your Kubernetes cluster as native **Kubernetes Secret** resources.
|
||||
This means any Pod, Deployment, or other Kubernetes resource can make use of dynamic secrets from Infisical just like any other K8s secret.
|
||||
## Overview
|
||||
|
||||
The **InfisicalDynamicSecret** CRD allows you to easily create and manage dynamic secret leases in Infisical and automatically sync them to your Kubernetes cluster as native **Kubernetes Secret** resources.
|
||||
This means any Pod, Deployment, or other Kubernetes resource can make use of dynamic secrets from Infisical just like any other K8s secret.
|
||||
|
||||
This CRD offers the following features:
|
||||
|
||||
- **Generate a dynamic secret lease** in Infisical and track its lifecycle.
|
||||
- **Write** the dynamic secret from Infisical to your cluster as native Kubernetes secret.
|
||||
- **Automatically rotate** the dynamic secret value before it expires to make sure your cluster always has valid credentials.
|
||||
- **Optionally trigger redeployments** of any workloads that consume the secret if you enable auto-reload.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- A project within Infisical.
|
||||
- A [machine identity](/docs/documentation/platform/identities/overview) ready for use in Infisical that has permissions to create dynamic secret leases in the project.
|
||||
- You have already configured a dynamic secret in Infisical.
|
||||
@ -77,16 +80,19 @@ spec:
|
||||
```
|
||||
|
||||
Apply the InfisicalDynamicSecret CRD to your cluster.
|
||||
|
||||
```bash
|
||||
kubectl apply -f dynamic-secret-crd.yaml
|
||||
```
|
||||
|
||||
After applying the InfisicalDynamicSecret CRD, you should notice that the dynamic secret lease has been created in Infisical and synced to your Kubernetes cluster. You can verify that the lease has been created by doing:
|
||||
|
||||
```bash
|
||||
kubectl get secret <managed-secret-name> -o yaml
|
||||
```
|
||||
|
||||
After getting the secret, you should should see that the secret has data that contains the lease credentials.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
data:
|
||||
@ -102,7 +108,7 @@ kind: Secret
|
||||
If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to
|
||||
` https://your-self-hosted-instace.com/api`
|
||||
|
||||
When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
|
||||
When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
|
||||
|
||||
<Accordion title="Advanced use case">
|
||||
If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet.
|
||||
@ -120,36 +126,45 @@ kind: Secret
|
||||
<Accordion title="leaseTTL">
|
||||
The `leaseTTL` is a string-formatted duration that defines the time the lease should last for the dynamic secret.
|
||||
|
||||
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
|
||||
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
|
||||
|
||||
The following units are supported:
|
||||
- `s` for seconds (must be at least 5 seconds)
|
||||
- `m` for minutes
|
||||
- `h` for hours
|
||||
- `d` for days
|
||||
The following units are supported:
|
||||
|
||||
<Note>
|
||||
The lease duration at most be 1 day (24 hours). And the TTL must be less than the max TTL defined on the dynamic secret.
|
||||
</Note>
|
||||
</Accordion>
|
||||
- `s` for seconds (must be at least 5 seconds)
|
||||
- `m` for minutes
|
||||
- `h` for hours
|
||||
- `d` for days
|
||||
|
||||
<Note>
|
||||
The lease duration at most be 1 day (24 hours). And the TTL must be less than the max TTL defined on the dynamic secret.
|
||||
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="managedSecretReference">
|
||||
The `managedSecretReference` field is used to define the Kubernetes secret where the dynamic secret lease should be stored. The required fields are `secretName` and `secretNamespace`.
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
managedSecretReference:
|
||||
secretName: <secret-name>
|
||||
secretNamespace: default
|
||||
```
|
||||
```yaml
|
||||
spec:
|
||||
managedSecretReference:
|
||||
secretName: <secret-name>
|
||||
secretNamespace: default
|
||||
```
|
||||
|
||||
<Accordion title="managedSecretReference.secretName">
|
||||
The name of the Kubernetes secret where the dynamic secret lease should be stored.
|
||||
</Accordion>
|
||||
{" "}
|
||||
|
||||
<Accordion title="managedSecretReference.secretNamespace">
|
||||
The namespace of the Kubernetes secret where the dynamic secret lease should be stored.
|
||||
</Accordion>
|
||||
<Accordion title="managedSecretReference.secretName">
|
||||
The name of the Kubernetes secret where the dynamic secret lease should be
|
||||
stored.
|
||||
</Accordion>
|
||||
|
||||
{" "}
|
||||
|
||||
<Accordion title="managedSecretReference.secretNamespace">
|
||||
The namespace of the Kubernetes secret where the dynamic secret lease should
|
||||
be stored.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="managedSecretReference.creationPolicy">
|
||||
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
|
||||
@ -165,32 +180,36 @@ kind: Secret
|
||||
</Tip>
|
||||
|
||||
This field is optional.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="managedSecretReference.secretType">
|
||||
Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets.
|
||||
|
||||
This field is optional.
|
||||
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="leaseRevocationPolicy">
|
||||
|
||||
The field is optional and will default to `None` if not defined.
|
||||
The field is optional and will default to `None` if not defined.
|
||||
|
||||
The lease revocation policy defines what the operator should do with the leases created by the operator, when the InfisicalDynamicSecret CRD is deleted.
|
||||
The lease revocation policy defines what the operator should do with the leases created by the operator, when the InfisicalDynamicSecret CRD is deleted.
|
||||
|
||||
Valid values are `None` and `Revoke`.
|
||||
Valid values are `None` and `Revoke`.
|
||||
|
||||
Behavior of each policy:
|
||||
- `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator.
|
||||
- `Revoke`: The operator will revoke the leases created by the operator when the InfisicalDynamicSecret CRD is deleted.
|
||||
Behavior of each policy:
|
||||
|
||||
- `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator.
|
||||
- `Revoke`: The operator will revoke the leases created by the operator when the InfisicalDynamicSecret CRD is deleted.
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
leaseRevocationPolicy: Revoke
|
||||
```
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
leaseRevocationPolicy: Revoke
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="dynamicSecret">
|
||||
@ -205,29 +224,37 @@ kind: Secret
|
||||
secretsPath: <secrets-path>
|
||||
```
|
||||
|
||||
<Accordion title="dynamicSecret.secretName">
|
||||
The name of the dynamic secret.
|
||||
</Accordion>
|
||||
{" "}
|
||||
|
||||
<Accordion title="dynamicSecret.projectId">
|
||||
The project ID of where the dynamic secret is stored in Infisical.
|
||||
</Accordion>
|
||||
<Accordion title="dynamicSecret.secretName">
|
||||
The name of the dynamic secret.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="dynamicSecret.environmentSlug">
|
||||
The environment slug of where the dynamic secret is stored in Infisical.
|
||||
</Accordion>
|
||||
{" "}
|
||||
|
||||
<Accordion title="dynamicSecret.secretsPath">
|
||||
The path of where the dynamic secret is stored in Infisical. The root path is `/`.
|
||||
</Accordion>
|
||||
<Accordion title="dynamicSecret.projectId">
|
||||
The project ID of where the dynamic secret is stored in Infisical.
|
||||
</Accordion>
|
||||
|
||||
{" "}
|
||||
|
||||
<Accordion title="dynamicSecret.environmentSlug">
|
||||
The environment slug of where the dynamic secret is stored in Infisical.
|
||||
</Accordion>
|
||||
|
||||
{" "}
|
||||
|
||||
<Accordion title="dynamicSecret.secretsPath">
|
||||
The path of where the dynamic secret is stored in Infisical. The root path is
|
||||
`/`.
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="authentication">
|
||||
|
||||
The `authentication` field dictates which authentication method to use when pushing secrets to Infisical.
|
||||
The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`.
|
||||
|
||||
The `authentication` field dictates which authentication method to use when pushing secrets to Infisical.
|
||||
The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`.
|
||||
|
||||
<Accordion title="universalAuth">
|
||||
The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider.
|
||||
@ -246,7 +273,7 @@ kind: Secret
|
||||
spec:
|
||||
universalAuth:
|
||||
credentialsRef:
|
||||
secretName: <secret-name>
|
||||
secretName: <secret-name>
|
||||
secretNamespace: <secret-namespace>
|
||||
```
|
||||
|
||||
@ -282,6 +309,7 @@ kind: Secret
|
||||
name: <secret-name>
|
||||
namespace: <secret-namespace>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="awsIamAuth">
|
||||
@ -316,12 +344,12 @@ kind: Secret
|
||||
azureAuth:
|
||||
identityId: <machine-identity-id>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="gcpIamAuth">
|
||||
The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments.
|
||||
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
|
||||
|
||||
|
||||
Valid fields:
|
||||
- `identityId`: The identity ID of the machine identity you created.
|
||||
- `serviceAccountKeyFilePath`: The path to the GCP service account key file.
|
||||
@ -334,6 +362,7 @@ kind: Secret
|
||||
identityId: <machine-identity-id>
|
||||
serviceAccountKeyFilePath: </path-to-service-account-key-file.json>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="gcpIdTokenAuth">
|
||||
The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments.
|
||||
@ -349,11 +378,11 @@ kind: Secret
|
||||
gcpIdTokenAuth:
|
||||
identityId: <machine-identity-id>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
<Accordion title="tls">
|
||||
This block defines the TLS settings to use for connecting to the Infisical
|
||||
instance.
|
||||
@ -376,11 +405,11 @@ kind: Secret
|
||||
secretNamespace: default
|
||||
key: ca.crt
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
### Applying the InfisicalDynamicSecret CRD to your cluster
|
||||
|
||||
Once you have configured the `InfisicalDynamicSecret` CRD with the required fields, you can apply it to your cluster. After applying, you should notice that a lease has been created in Infisical and synced to your Kubernetes cluster.
|
||||
@ -396,7 +425,7 @@ To address this, we've added functionality to automatically redeploy your deploy
|
||||
|
||||
#### Enabling auto redeploy
|
||||
|
||||
To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret
|
||||
To enable auto redeployment you simply have to add the following annotation to the deployment, statefulset, or daemonset that consumes a managed secret.
|
||||
|
||||
```yaml
|
||||
secrets.infisical.com/auto-reload: "true"
|
||||
|
@ -547,12 +547,14 @@ The `managedSecretReference` field is deprecated and will be removed in a future
|
||||
Replace it with `managedKubeSecretReferences`, which now accepts an array of references to support multiple managed secrets in a single InfisicalSecret CRD.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
managedKubeSecretReferences:
|
||||
- secretName: managed-secret
|
||||
secretNamespace: default
|
||||
creationPolicy: "Orphan"
|
||||
managedKubeSecretReferences:
|
||||
- secretName: managed-secret
|
||||
secretNamespace: default
|
||||
creationPolicy: "Orphan"
|
||||
```
|
||||
|
||||
</Note>
|
||||
|
||||
<Accordion title="managedKubeSecretReferences">
|
||||
@ -666,13 +668,13 @@ The example below assumes that the `BINARY_KEY_BASE64` secret is stored as a bas
|
||||
The resulting managed secret will contain the decoded value of `BINARY_KEY_BASE64`.
|
||||
|
||||
```yaml
|
||||
managedKubeSecretReferences:
|
||||
secretName: managed-secret
|
||||
secretNamespace: default
|
||||
template:
|
||||
includeAllSecrets: true
|
||||
data:
|
||||
BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
|
||||
managedKubeSecretReferences:
|
||||
secretName: managed-secret
|
||||
secretNamespace: default
|
||||
template:
|
||||
includeAllSecrets: true
|
||||
data:
|
||||
BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
@ -866,7 +868,7 @@ To address this, we added functionality to automatically redeploy your deploymen
|
||||
|
||||
#### Enabling auto redeploy
|
||||
|
||||
To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret
|
||||
To enable auto redeployment you simply have to add the following annotation to the deployment, statefulset, or daemonset that consumes a managed secret.
|
||||
|
||||
```yaml
|
||||
secrets.infisical.com/auto-reload: "true"
|
||||
@ -948,4 +950,4 @@ metadata:
|
||||
type: Opaque
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
@ -75,7 +75,7 @@ via the UI or API for the third-party service you intend to sync secrets to.
|
||||
2. <strong>Create Secret Sync:</strong> Configure a Secret Sync in the desired project by specifying the following parameters via the UI or API:
|
||||
- <strong>Source:</strong> The project environment and folder path you wish to retrieve secrets from.
|
||||
- <strong>Destination:</strong> The App Connection to utilize and the destination endpoint to deploy secrets to. These can vary between services.
|
||||
- <strong>Options:</strong> Customize how secrets should be synced. Examples include adding a suffix or prefix to your secrets, or importing secrets from the destination on the initial sync.
|
||||
- <strong>Options:</strong> Customize how secrets should be synced, such as whether or not secrets should be imported from the destination on the initial sync.
|
||||
|
||||
<Note>
|
||||
Secret Syncs are the source of truth for connected third-party services. Any secret,
|
||||
|
@ -201,6 +201,10 @@
|
||||
"documentation/platform/dynamic-secrets/totp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Gateway",
|
||||
"pages": ["documentation/platform/gateways/overview"]
|
||||
},
|
||||
"documentation/platform/project-templates",
|
||||
{
|
||||
"group": "Workflow Integrations",
|
||||
|
1271
frontend/package-lock.json
generated
1271
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -58,6 +58,7 @@
|
||||
"classnames": "^2.5.1",
|
||||
"cva": "npm:class-variance-authority@^0.7.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^11.14.1",
|
||||
"i18next": "^24.1.0",
|
||||
@ -79,9 +80,11 @@
|
||||
"react-hook-form": "^7.54.0",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^10.0.1",
|
||||
"react-select": "^5.9.0",
|
||||
"react-toastify": "^10.0.6",
|
||||
"redaxios": "^0.5.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
|
1
frontend/public/lotties/gateway.json
Normal file
1
frontend/public/lotties/gateway.json
Normal file
File diff suppressed because one or more lines are too long
20
frontend/src/components/page-frames/Banner.tsx
Normal file
20
frontend/src/components/page-frames/Banner.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import DOMPurify from "dompurify";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
import { useServerConfig } from "@app/context";
|
||||
|
||||
export const Banner = () => {
|
||||
const { config } = useServerConfig();
|
||||
|
||||
// eslint-disable-next-line react/no-danger-with-children
|
||||
return config.pageFrameContent ? (
|
||||
<div className="h-[3vh] w-full text-center">
|
||||
<ReactMarkdown rehypePlugins={[rehypeRaw]}>
|
||||
{DOMPurify.sanitize(config.pageFrameContent)}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
@ -45,7 +45,11 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
)}
|
||||
style={{ maxHeight: "90%" }}
|
||||
>
|
||||
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
|
||||
{title && (
|
||||
<DialogPrimitive.Title>
|
||||
<CardTitle subTitle={subTitle}>{title}</CardTitle>
|
||||
</DialogPrimitive.Title>
|
||||
)}
|
||||
<CardBody
|
||||
className={twMerge("overflow-y-auto overflow-x-hidden", bodyClassName)}
|
||||
style={{ maxHeight: "90%" }}
|
||||
|
@ -25,6 +25,7 @@ export const publicPaths = [
|
||||
"/login/sso",
|
||||
"/admin/signup",
|
||||
"/shared/secret/[id]",
|
||||
"/secret-request/secret/[id]",
|
||||
"/share-secret"
|
||||
];
|
||||
|
||||
@ -59,7 +60,8 @@ export const leaveConfirmDefaultMessage =
|
||||
|
||||
export enum SessionStorageKeys {
|
||||
CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN",
|
||||
ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL"
|
||||
ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL",
|
||||
AUTH_CONSENT = "AUTH_CONSENT"
|
||||
}
|
||||
|
||||
export const secretTagsColors = [
|
||||
|
@ -21,6 +21,10 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
"/organization/secret-scanning",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
|
||||
),
|
||||
SecretSharing: setRoute(
|
||||
"/organization/secret-sharing",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
|
||||
),
|
||||
SettingsPage: setRoute(
|
||||
"/organization/settings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings"
|
||||
@ -285,6 +289,10 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
)
|
||||
},
|
||||
Public: {
|
||||
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId")
|
||||
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId"),
|
||||
ViewSecretRequestByIDPage: setRoute(
|
||||
"/secret-request/secret/$secretRequestId",
|
||||
"/secret-request/secret/$secretRequestId"
|
||||
)
|
||||
}
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user