mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-23 03:03:05 +00:00
Compare commits
177 Commits
upgrade-in
...
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 | |||
fb030401ab | |||
f4bd48fd1d | |||
177ccf6c9e | |||
9200137d6c | |||
a196028064 | |||
0c0e20f00e | |||
710429c805 | |||
c121bd930b | |||
87d383a9c4 | |||
6e590a78a0 | |||
ab4b6c17b3 | |||
27cd40c8ce | |||
5f089e0b9d | |||
19940522aa | |||
28b18c1cb1 | |||
7ae2cc2db8 | |||
97c069bc0f | |||
4a51b4d619 | |||
478e0c5ff5 | |||
5c08136fca | |||
cb8528adc4 | |||
d7935d30ce | |||
ac3bab3074 | |||
4a44b7857e | |||
63b8301065 | |||
babe70e00f | |||
2ba834b036 | |||
db7a6f3530 | |||
f23ea0991c | |||
d80a104f7b | |||
f8ab2bcdfd | |||
d980d471e8 | |||
9cdb4dcde9 | |||
3583a09ab5 | |||
86b6d23f34 | |||
2c31ac0569 | |||
d6c1b8e30c | |||
0d4d73b61d | |||
198b607e2e | |||
f0e6bcef9b | |||
69fb87bbfc | |||
b0cd5bd10d | |||
15119ffda9 | |||
4df409e627 | |||
3a5a88467d | |||
012f265363 | |||
823bf134dd | |||
1f5a73047d | |||
0366df6e19 | |||
c77e0c0666 | |||
8e70731c4c | |||
1db8c9ea29 | |||
21c6700db2 | |||
619062033b | |||
92b3b9157a | |||
f6cd78e078 | |||
36973b1b5c | |||
1ca578ee03 | |||
8a7f7ac9fd | |||
049fd8e769 | |||
8e2cce865a | |||
943c2b0e69 | |||
8518fed726 | |||
5148435f5f | |||
2c825616a6 | |||
febbd4ade5 | |||
603b740bbe | |||
874dc01692 | |||
b44b8bf647 | |||
af652f7e52 | |||
b8a07979c3 | |||
292c9051bd | |||
77fac45df1 | |||
0ab90383c2 | |||
a3acfa65a2 | |||
0269f57768 | |||
9f9ded5102 | |||
8b315c946c | |||
dd9a7755bc | |||
64c2fba350 | |||
c7f80f7d9e | |||
ecf2cb6e51 | |||
1e5a9a6020 |
.env.exampleDockerfile.fips.standalone-infisicalDockerfile.standalone-infisicalmint.json
backend
Dockerfile.dev
e2e-test/routes/v3
package-lock.jsonpackage.jsonsrc
@types
db
knexfile.ts
migrations
20250203141127_add-kmip.ts20250212191958_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
gateway-router.tsindex.tskmip-router.tskmip-spec-router.tssecret-approval-request-router.tssnapshot-router.ts
services
audit-log
dynamic-secret
gateway
kmip
kmip-client-certificate-dal.tskmip-client-dal.tskmip-enum.tskmip-operation-service.tskmip-org-config-dal.tskmip-org-server-certificate-dal.tskmip-service.tskmip-types.ts
license
permission
secret-approval-request
ssh-certificate
keystore
lib
api-docs
config
crypto
gateway
ip
telemetry
turn
server/routes
index.ts
v1
admin-router.ts
app-connection-routers
auth-router.tsindex.tssecret-folder-router.tssecret-requests-router.tssecret-sharing-router.tsv3
services
app-connection
auth-token
auth
certificate-authority
certificate
integration-auth
kms
resource-cleanup
secret-folder
secret-sharing
secret-sync
aws-parameter-store
aws-secrets-manager
secret-sync-queue.tssecret-sync-schemas.tssecret-sync-types.tssecret-v2-bridge
secret
smtp
telemetry
cli
company/handbook
docker-compose.prod.ymldocs
api-reference/overview
documentation
platform
setup
images
app-connections/aws
platform
admin-panels
gateways
kms/kmip
secret-syncs
aws-parameter-store
aws-secrets-manager
integrations
app-connections
frameworks
platforms/kubernetes
secret-syncs
sdks/languages
self-hosting
frontend
package-lock.jsonpackage.jsonconst.tsrouteTree.gen.tsroutes.ts
public/lotties
src
components
page-frames
secret-syncs/forms
CreateSecretSyncForm.tsxEditSecretSyncForm.tsx
SecretSyncDestinationFields
SecretSyncOptionsFields
AwsParameterStoreSyncOptionsFields.tsxAwsSecretsManagerSyncOptionsFields.tsxSecretSyncOptionsFields.tsxindex.ts
SecretSyncReviewFields
AwsParameterStoreSyncReviewFields.tsxAwsSecretsManagerSyncReviewFields.tsxSecretSyncReviewFields.tsx
schemas
aws-parameter-store-sync-destination-schema.tsaws-secrets-manager-sync-destination-schema.tsazure-app-configuration-sync-destination-schema.tsazure-key-vault-sync-destination-schema.tsbase-secret-sync-schema.tsdatabricks-sync-destination-schema.tsgcp-sync-destination-schema.tsgithub-sync-destination-schema.tssecret-sync-schema.ts
v2
const
context
helpers
hooks
api
admin
appConnections/aws
auditLogs
dynamicSecret
gateways
index.tsxkmip
secretFolders
secretSharing
secretSyncs/types
secrets
subscriptions
users
utils
layouts
main.tsxpages
admin/OverviewPage
auth
kms/KmipPage
KmipPage.tsx
components
CreateKmipClientCertificateModal.tsxDeleteKmipClientModal.tsxKmipClientCertificateModal.tsxKmipClientModal.tsxKmipClientTable.tsx
route.tsxmiddlewares
organization
AuditLogsPage/components
Gateways/GatewayListPage
NoOrgPage
RoleByIDPage/components
OrgRoleModifySection.utils.ts
RolePermissionsSection
SecretSharingPage
SecretSharingPage.tsxShareSecretSection.tsxroute.tsx
components
RequestSecret
AddSecretRequestModal.tsxRequestSecretForm.tsxRequestSecretTab.tsxRequestedSecretsRow.tsxRequestedSecretsTable.tsxRevealSecretValueModal.tsx
ShareSecret
index.tsxSettingsPage/components
project/RoleDetailsBySlugPage/components
public
ShareSecretPage/components
ViewSecretRequestByIDPage
secret-manager
IntegrationsListPage/components/SecretSyncsTab
OverviewPage
SecretDashboardPage/components
ActionBar
DynamicSecretListView/EditDynamicSecretForm
FolderListView
PitDrawer
SecretListView
SecretSyncDetailsByIDPage
SecretSyncDetailsByIDPage.tsx
components
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
|
||||
|
||||
|
@ -535,6 +535,107 @@ describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }]
|
||||
);
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Bulk upsert secrets in path $path", async ({ secret, path }) => {
|
||||
const updateSharedSecRes = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v3/secrets/batch/raw`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
mode: "upsert",
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: "update-value",
|
||||
secretComment: secret.comment
|
||||
}))
|
||||
}
|
||||
});
|
||||
expect(updateSharedSecRes.statusCode).toBe(200);
|
||||
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
|
||||
expect(updateSharedSecPayload).toHaveProperty("secrets");
|
||||
|
||||
// bulk ones should exist
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining(
|
||||
Array.from(Array(5)).map((_e, i) =>
|
||||
expect.objectContaining({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: "update-value",
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
|
||||
);
|
||||
});
|
||||
|
||||
test("Bulk upsert secrets in path multiple paths", async () => {
|
||||
const firstBatchSecrets = Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `BULK-KEY-${secretTestCases[0].secret.key}-${i + 1}`,
|
||||
secretValue: "update-value",
|
||||
secretComment: "comment",
|
||||
secretPath: secretTestCases[0].path
|
||||
}));
|
||||
const secondBatchSecrets = Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `BULK-KEY-${secretTestCases[1].secret.key}-${i + 1}`,
|
||||
secretValue: "update-value",
|
||||
secretComment: "comment",
|
||||
secretPath: secretTestCases[1].path
|
||||
}));
|
||||
const testSecrets = [...firstBatchSecrets, ...secondBatchSecrets];
|
||||
|
||||
const updateSharedSecRes = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v3/secrets/batch/raw`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
mode: "upsert",
|
||||
secrets: testSecrets
|
||||
}
|
||||
});
|
||||
expect(updateSharedSecRes.statusCode).toBe(200);
|
||||
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
|
||||
expect(updateSharedSecPayload).toHaveProperty("secrets");
|
||||
|
||||
// bulk ones should exist
|
||||
const firstBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[0].path);
|
||||
expect(firstBatchSecretsOnInfisical).toEqual(
|
||||
expect.arrayContaining(
|
||||
firstBatchSecrets.map((el) =>
|
||||
expect.objectContaining({
|
||||
secretKey: el.secretKey,
|
||||
secretValue: "update-value",
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
const secondBatchSecretsOnInfisical = await getSecrets(seedData1.environment.slug, secretTestCases[1].path);
|
||||
expect(secondBatchSecretsOnInfisical).toEqual(
|
||||
expect.arrayContaining(
|
||||
secondBatchSecrets.map((el) =>
|
||||
expect.objectContaining({
|
||||
secretKey: el.secretKey,
|
||||
secretValue: "update-value",
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
await Promise.all(testSecrets.map((el) => deleteSecret({ path: el.secretPath, key: el.secretKey })));
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Bulk delete secrets in path $path", async ({ secret, path }) => {
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path }))
|
||||
|
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",
|
||||
|
13
backend/src/@types/fastify.d.ts
vendored
13
backend/src/@types/fastify.d.ts
vendored
@ -13,9 +13,13 @@ 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";
|
||||
import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
|
||||
import { TKmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service";
|
||||
import { TKmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
|
||||
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
||||
@ -126,6 +130,11 @@ declare module "fastify" {
|
||||
isUserCompleted: string;
|
||||
providerAuthToken: string;
|
||||
};
|
||||
kmipUser: {
|
||||
projectId: string;
|
||||
clientId: string;
|
||||
name: string;
|
||||
};
|
||||
auditLogInfo: Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
|
||||
ssoConfig: Awaited<ReturnType<TSamlConfigServiceFactory["getSaml"]>>;
|
||||
ldapConfig: Awaited<ReturnType<TLdapConfigServiceFactory["getLdapCfg"]>>;
|
||||
@ -218,11 +227,15 @@ declare module "fastify" {
|
||||
totp: TTotpServiceFactory;
|
||||
appConnection: TAppConnectionServiceFactory;
|
||||
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
|
||||
store: {
|
||||
user: Pick<TUserDALFactory, "findById">;
|
||||
kmipClient: Pick<TKmipClientDALFactory, "findByProjectAndClientId">;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
48
backend/src/@types/knex.d.ts
vendored
48
backend/src/@types/knex.d.ts
vendored
@ -68,6 +68,9 @@ import {
|
||||
TExternalKms,
|
||||
TExternalKmsInsert,
|
||||
TExternalKmsUpdate,
|
||||
TGateways,
|
||||
TGatewaysInsert,
|
||||
TGatewaysUpdate,
|
||||
TGitAppInstallSessions,
|
||||
TGitAppInstallSessionsInsert,
|
||||
TGitAppInstallSessionsUpdate,
|
||||
@ -143,6 +146,18 @@ import {
|
||||
TInternalKms,
|
||||
TInternalKmsInsert,
|
||||
TInternalKmsUpdate,
|
||||
TKmipClientCertificates,
|
||||
TKmipClientCertificatesInsert,
|
||||
TKmipClientCertificatesUpdate,
|
||||
TKmipClients,
|
||||
TKmipClientsInsert,
|
||||
TKmipClientsUpdate,
|
||||
TKmipOrgConfigs,
|
||||
TKmipOrgConfigsInsert,
|
||||
TKmipOrgConfigsUpdate,
|
||||
TKmipOrgServerCertificates,
|
||||
TKmipOrgServerCertificatesInsert,
|
||||
TKmipOrgServerCertificatesUpdate,
|
||||
TKmsKeys,
|
||||
TKmsKeysInsert,
|
||||
TKmsKeysUpdate,
|
||||
@ -167,6 +182,9 @@ import {
|
||||
TOrgBots,
|
||||
TOrgBotsInsert,
|
||||
TOrgBotsUpdate,
|
||||
TOrgGatewayConfig,
|
||||
TOrgGatewayConfigInsert,
|
||||
TOrgGatewayConfigUpdate,
|
||||
TOrgMemberships,
|
||||
TOrgMembershipsInsert,
|
||||
TOrgMembershipsUpdate,
|
||||
@ -188,6 +206,9 @@ import {
|
||||
TProjectEnvironments,
|
||||
TProjectEnvironmentsInsert,
|
||||
TProjectEnvironmentsUpdate,
|
||||
TProjectGateways,
|
||||
TProjectGatewaysInsert,
|
||||
TProjectGatewaysUpdate,
|
||||
TProjectKeys,
|
||||
TProjectKeysInsert,
|
||||
TProjectKeysUpdate,
|
||||
@ -902,5 +923,32 @@ declare module "knex/types/tables" {
|
||||
TAppConnectionsUpdate
|
||||
>;
|
||||
[TableName.SecretSync]: KnexOriginal.CompositeTableType<TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate>;
|
||||
[TableName.KmipClient]: KnexOriginal.CompositeTableType<TKmipClients, TKmipClientsInsert, TKmipClientsUpdate>;
|
||||
[TableName.KmipOrgConfig]: KnexOriginal.CompositeTableType<
|
||||
TKmipOrgConfigs,
|
||||
TKmipOrgConfigsInsert,
|
||||
TKmipOrgConfigsUpdate
|
||||
>;
|
||||
[TableName.KmipOrgServerCertificates]: KnexOriginal.CompositeTableType<
|
||||
TKmipOrgServerCertificates,
|
||||
TKmipOrgServerCertificatesInsert,
|
||||
TKmipOrgServerCertificatesUpdate
|
||||
>;
|
||||
[TableName.KmipClientCertificates]: KnexOriginal.CompositeTableType<
|
||||
TKmipClientCertificates,
|
||||
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;
|
||||
|
108
backend/src/db/migrations/20250203141127_add-kmip.ts
Normal file
108
backend/src/db/migrations/20250203141127_add-kmip.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient);
|
||||
if (!hasKmipClientTable) {
|
||||
await knex.schema.createTable(TableName.KmipClient, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name").notNullable();
|
||||
t.specificType("permissions", "text[]");
|
||||
t.string("description");
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig);
|
||||
if (!hasKmipOrgPkiConfig) {
|
||||
await knex.schema.createTable(TableName.KmipOrgConfig, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.unique("orgId");
|
||||
|
||||
t.string("caKeyAlgorithm").notNullable();
|
||||
|
||||
t.datetime("rootCaIssuedAt").notNullable();
|
||||
t.datetime("rootCaExpiration").notNullable();
|
||||
t.string("rootCaSerialNumber").notNullable();
|
||||
t.binary("encryptedRootCaCertificate").notNullable();
|
||||
t.binary("encryptedRootCaPrivateKey").notNullable();
|
||||
|
||||
t.datetime("serverIntermediateCaIssuedAt").notNullable();
|
||||
t.datetime("serverIntermediateCaExpiration").notNullable();
|
||||
t.string("serverIntermediateCaSerialNumber");
|
||||
t.binary("encryptedServerIntermediateCaCertificate").notNullable();
|
||||
t.binary("encryptedServerIntermediateCaChain").notNullable();
|
||||
t.binary("encryptedServerIntermediateCaPrivateKey").notNullable();
|
||||
|
||||
t.datetime("clientIntermediateCaIssuedAt").notNullable();
|
||||
t.datetime("clientIntermediateCaExpiration").notNullable();
|
||||
t.string("clientIntermediateCaSerialNumber").notNullable();
|
||||
t.binary("encryptedClientIntermediateCaCertificate").notNullable();
|
||||
t.binary("encryptedClientIntermediateCaChain").notNullable();
|
||||
t.binary("encryptedClientIntermediateCaPrivateKey").notNullable();
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.KmipOrgConfig);
|
||||
}
|
||||
|
||||
const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates);
|
||||
if (!hasKmipOrgServerCertTable) {
|
||||
await knex.schema.createTable(TableName.KmipOrgServerCertificates, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.string("commonName").notNullable();
|
||||
t.string("altNames").notNullable();
|
||||
t.string("serialNumber").notNullable();
|
||||
t.string("keyAlgorithm").notNullable();
|
||||
t.datetime("issuedAt").notNullable();
|
||||
t.datetime("expiration").notNullable();
|
||||
t.binary("encryptedCertificate").notNullable();
|
||||
t.binary("encryptedChain").notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates);
|
||||
if (!hasKmipClientCertTable) {
|
||||
await knex.schema.createTable(TableName.KmipClientCertificates, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("kmipClientId").notNullable();
|
||||
t.foreign("kmipClientId").references("id").inTable(TableName.KmipClient).onDelete("CASCADE");
|
||||
t.string("serialNumber").notNullable();
|
||||
t.string("keyAlgorithm").notNullable();
|
||||
t.datetime("issuedAt").notNullable();
|
||||
t.datetime("expiration").notNullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasKmipOrgPkiConfig = await knex.schema.hasTable(TableName.KmipOrgConfig);
|
||||
if (hasKmipOrgPkiConfig) {
|
||||
await knex.schema.dropTable(TableName.KmipOrgConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.KmipOrgConfig);
|
||||
}
|
||||
|
||||
const hasKmipOrgServerCertTable = await knex.schema.hasTable(TableName.KmipOrgServerCertificates);
|
||||
if (hasKmipOrgServerCertTable) {
|
||||
await knex.schema.dropTable(TableName.KmipOrgServerCertificates);
|
||||
}
|
||||
|
||||
const hasKmipClientCertTable = await knex.schema.hasTable(TableName.KmipClientCertificates);
|
||||
if (hasKmipClientCertTable) {
|
||||
await knex.schema.dropTable(TableName.KmipClientCertificates);
|
||||
}
|
||||
|
||||
const hasKmipClientTable = await knex.schema.hasTable(TableName.KmipClient);
|
||||
if (hasKmipClientTable) {
|
||||
await knex.schema.dropTable(TableName.KmipClient);
|
||||
}
|
||||
}
|
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";
|
||||
@ -45,6 +46,10 @@ export * from "./incident-contacts";
|
||||
export * from "./integration-auths";
|
||||
export * from "./integrations";
|
||||
export * from "./internal-kms";
|
||||
export * from "./kmip-client-certificates";
|
||||
export * from "./kmip-clients";
|
||||
export * from "./kmip-org-configs";
|
||||
export * from "./kmip-org-server-certificates";
|
||||
export * from "./kms-key-versions";
|
||||
export * from "./kms-keys";
|
||||
export * from "./kms-root-config";
|
||||
@ -53,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";
|
||||
@ -61,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";
|
||||
|
23
backend/src/db/schemas/kmip-client-certificates.ts
Normal file
23
backend/src/db/schemas/kmip-client-certificates.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// 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 KmipClientCertificatesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
kmipClientId: z.string().uuid(),
|
||||
serialNumber: z.string(),
|
||||
keyAlgorithm: z.string(),
|
||||
issuedAt: z.date(),
|
||||
expiration: z.date()
|
||||
});
|
||||
|
||||
export type TKmipClientCertificates = z.infer<typeof KmipClientCertificatesSchema>;
|
||||
export type TKmipClientCertificatesInsert = Omit<z.input<typeof KmipClientCertificatesSchema>, TImmutableDBKeys>;
|
||||
export type TKmipClientCertificatesUpdate = Partial<
|
||||
Omit<z.input<typeof KmipClientCertificatesSchema>, TImmutableDBKeys>
|
||||
>;
|
20
backend/src/db/schemas/kmip-clients.ts
Normal file
20
backend/src/db/schemas/kmip-clients.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 KmipClientsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
permissions: z.string().array().nullable().optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
projectId: z.string()
|
||||
});
|
||||
|
||||
export type TKmipClients = z.infer<typeof KmipClientsSchema>;
|
||||
export type TKmipClientsInsert = Omit<z.input<typeof KmipClientsSchema>, TImmutableDBKeys>;
|
||||
export type TKmipClientsUpdate = Partial<Omit<z.input<typeof KmipClientsSchema>, TImmutableDBKeys>>;
|
39
backend/src/db/schemas/kmip-org-configs.ts
Normal file
39
backend/src/db/schemas/kmip-org-configs.ts
Normal file
@ -0,0 +1,39 @@
|
||||
// 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 KmipOrgConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
orgId: z.string().uuid(),
|
||||
caKeyAlgorithm: z.string(),
|
||||
rootCaIssuedAt: z.date(),
|
||||
rootCaExpiration: z.date(),
|
||||
rootCaSerialNumber: z.string(),
|
||||
encryptedRootCaCertificate: zodBuffer,
|
||||
encryptedRootCaPrivateKey: zodBuffer,
|
||||
serverIntermediateCaIssuedAt: z.date(),
|
||||
serverIntermediateCaExpiration: z.date(),
|
||||
serverIntermediateCaSerialNumber: z.string().nullable().optional(),
|
||||
encryptedServerIntermediateCaCertificate: zodBuffer,
|
||||
encryptedServerIntermediateCaChain: zodBuffer,
|
||||
encryptedServerIntermediateCaPrivateKey: zodBuffer,
|
||||
clientIntermediateCaIssuedAt: z.date(),
|
||||
clientIntermediateCaExpiration: z.date(),
|
||||
clientIntermediateCaSerialNumber: z.string(),
|
||||
encryptedClientIntermediateCaCertificate: zodBuffer,
|
||||
encryptedClientIntermediateCaChain: zodBuffer,
|
||||
encryptedClientIntermediateCaPrivateKey: zodBuffer,
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TKmipOrgConfigs = z.infer<typeof KmipOrgConfigsSchema>;
|
||||
export type TKmipOrgConfigsInsert = Omit<z.input<typeof KmipOrgConfigsSchema>, TImmutableDBKeys>;
|
||||
export type TKmipOrgConfigsUpdate = Partial<Omit<z.input<typeof KmipOrgConfigsSchema>, TImmutableDBKeys>>;
|
29
backend/src/db/schemas/kmip-org-server-certificates.ts
Normal file
29
backend/src/db/schemas/kmip-org-server-certificates.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 KmipOrgServerCertificatesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
orgId: z.string().uuid(),
|
||||
commonName: z.string(),
|
||||
altNames: z.string(),
|
||||
serialNumber: z.string(),
|
||||
keyAlgorithm: z.string(),
|
||||
issuedAt: z.date(),
|
||||
expiration: z.date(),
|
||||
encryptedCertificate: zodBuffer,
|
||||
encryptedChain: zodBuffer
|
||||
});
|
||||
|
||||
export type TKmipOrgServerCertificates = z.infer<typeof KmipOrgServerCertificatesSchema>;
|
||||
export type TKmipOrgServerCertificatesInsert = Omit<z.input<typeof KmipOrgServerCertificatesSchema>, TImmutableDBKeys>;
|
||||
export type TKmipOrgServerCertificatesUpdate = Partial<
|
||||
Omit<z.input<typeof KmipOrgServerCertificatesSchema>, TImmutableDBKeys>
|
||||
>;
|
@ -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",
|
||||
@ -132,7 +136,11 @@ export enum TableName {
|
||||
SlackIntegrations = "slack_integrations",
|
||||
ProjectSlackConfigs = "project_slack_configs",
|
||||
AppConnection = "app_connections",
|
||||
SecretSync = "secret_syncs"
|
||||
SecretSync = "secret_syncs",
|
||||
KmipClient = "kmip_clients",
|
||||
KmipOrgConfig = "kmip_org_configs",
|
||||
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||
KmipClientCertificates = "kmip_client_certificates"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
|
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,8 +7,11 @@ 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";
|
||||
import { registerKmipSpecRouter } from "./kmip-spec-router";
|
||||
import { registerLdapRouter } from "./ldap-router";
|
||||
import { registerLicenseRouter } from "./license-router";
|
||||
import { registerOidcRouter } from "./oidc-router";
|
||||
@ -65,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" });
|
||||
@ -110,4 +115,12 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
|
||||
|
||||
await server.register(
|
||||
async (kmipRouter) => {
|
||||
await kmipRouter.register(registerKmipRouter);
|
||||
await kmipRouter.register(registerKmipSpecRouter, { prefix: "/spec" });
|
||||
},
|
||||
{ prefix: "/kmip" }
|
||||
);
|
||||
};
|
||||
|
428
backend/src/ee/routes/v1/kmip-router.ts
Normal file
428
backend/src/ee/routes/v1/kmip-router.ts
Normal file
@ -0,0 +1,428 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { KmipClientsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { KmipPermission } from "@app/ee/services/kmip/kmip-enum";
|
||||
import { KmipClientOrderBy } from "@app/ee/services/kmip/kmip-types";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
import { validateAltNamesField } from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
|
||||
const KmipClientResponseSchema = KmipClientsSchema.pick({
|
||||
projectId: true,
|
||||
name: true,
|
||||
id: true,
|
||||
description: true,
|
||||
permissions: true
|
||||
});
|
||||
|
||||
export const registerKmipRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/clients",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
projectId: z.string(),
|
||||
name: z.string().trim().min(1),
|
||||
description: z.string().optional(),
|
||||
permissions: z.nativeEnum(KmipPermission).array()
|
||||
}),
|
||||
response: {
|
||||
200: KmipClientResponseSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const kmipClient = await server.services.kmip.createKmipClient({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: kmipClient.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_KMIP_CLIENT,
|
||||
metadata: {
|
||||
id: kmipClient.id,
|
||||
name: kmipClient.name,
|
||||
permissions: (kmipClient.permissions ?? []) as KmipPermission[]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return kmipClient;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/clients/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().min(1),
|
||||
description: z.string().optional(),
|
||||
permissions: z.nativeEnum(KmipPermission).array()
|
||||
}),
|
||||
response: {
|
||||
200: KmipClientResponseSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const kmipClient = await server.services.kmip.updateKmipClient({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.params,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: kmipClient.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_KMIP_CLIENT,
|
||||
metadata: {
|
||||
id: kmipClient.id,
|
||||
name: kmipClient.name,
|
||||
permissions: (kmipClient.permissions ?? []) as KmipPermission[]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return kmipClient;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/clients/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: KmipClientResponseSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const kmipClient = await server.services.kmip.deleteKmipClient({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.params
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: kmipClient.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_KMIP_CLIENT,
|
||||
metadata: {
|
||||
id: kmipClient.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return kmipClient;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/clients/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: KmipClientResponseSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const kmipClient = await server.services.kmip.getKmipClient({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.params
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: kmipClient.projectId,
|
||||
event: {
|
||||
type: EventType.GET_KMIP_CLIENT,
|
||||
metadata: {
|
||||
id: kmipClient.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return kmipClient;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/clients",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List KMIP clients",
|
||||
querystring: z.object({
|
||||
projectId: z.string(),
|
||||
offset: z.coerce.number().min(0).optional().default(0),
|
||||
limit: z.coerce.number().min(1).max(100).optional().default(100),
|
||||
orderBy: z.nativeEnum(KmipClientOrderBy).optional().default(KmipClientOrderBy.Name),
|
||||
orderDirection: z.nativeEnum(OrderByDirection).optional().default(OrderByDirection.ASC),
|
||||
search: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
kmipClients: KmipClientResponseSchema.array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { kmipClients, totalCount } = await server.services.kmip.listKmipClientsByProjectId({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.GET_KMIP_CLIENTS,
|
||||
metadata: {
|
||||
ids: kmipClients.map((key) => key.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { kmipClients, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/clients/:id/certificates",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm),
|
||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
serialNumber: z.string(),
|
||||
certificateChain: z.string(),
|
||||
certificate: z.string(),
|
||||
privateKey: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const certificate = await server.services.kmip.createKmipClientCertificate({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
clientId: req.params.id,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: certificate.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE,
|
||||
metadata: {
|
||||
clientId: req.params.id,
|
||||
serialNumber: certificate.serialNumber,
|
||||
ttl: req.body.ttl,
|
||||
keyAlgorithm: req.body.keyAlgorithm
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return certificate;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
caKeyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
serverCertificateChain: z.string(),
|
||||
clientCertificateChain: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const chains = await server.services.kmip.setupOrgKmip({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.SETUP_KMIP,
|
||||
metadata: {
|
||||
keyAlgorithm: req.body.caKeyAlgorithm
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return chains;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
serverCertificateChain: z.string(),
|
||||
clientCertificateChain: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const kmip = await server.services.kmip.getOrgKmip({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.GET_KMIP,
|
||||
metadata: {
|
||||
id: kmip.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return kmip;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/server-registration",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
hostnamesOrIps: validateAltNamesField,
|
||||
commonName: z.string().trim().min(1).optional(),
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional().default(CertKeyAlgorithm.RSA_2048),
|
||||
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
clientCertificateChain: z.string(),
|
||||
certificateChain: z.string(),
|
||||
certificate: z.string(),
|
||||
privateKey: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const configs = await server.services.kmip.registerServer({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.REGISTER_KMIP_SERVER,
|
||||
metadata: {
|
||||
serverCertificateSerialNumber: configs.serverCertificateSerialNumber,
|
||||
hostnamesOrIps: req.body.hostnamesOrIps,
|
||||
commonName: req.body.commonName ?? "kmip-server",
|
||||
keyAlgorithm: req.body.keyAlgorithm,
|
||||
ttl: req.body.ttl
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return configs;
|
||||
}
|
||||
});
|
||||
};
|
477
backend/src/ee/routes/v1/kmip-spec-router.ts
Normal file
477
backend/src/ee/routes/v1/kmip-spec-router.ts
Normal file
@ -0,0 +1,477 @@
|
||||
import z from "zod";
|
||||
|
||||
import { KmsKeysSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||
server.decorateRequest("kmipUser", null);
|
||||
|
||||
server.addHook("onRequest", async (req) => {
|
||||
const clientId = req.headers["x-kmip-client-id"] as string;
|
||||
const projectId = req.headers["x-kmip-project-id"] as string;
|
||||
const clientCertSerialNumber = req.headers["x-kmip-client-certificate-serial-number"] as string;
|
||||
const serverCertSerialNumber = req.headers["x-kmip-server-certificate-serial-number"] as string;
|
||||
|
||||
if (!serverCertSerialNumber) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Missing server certificate serial number from request"
|
||||
});
|
||||
}
|
||||
|
||||
if (!clientCertSerialNumber) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Missing client certificate serial number from request"
|
||||
});
|
||||
}
|
||||
|
||||
if (!clientId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Missing client ID from request"
|
||||
});
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Missing project ID from request"
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: assert that server certificate used is not revoked
|
||||
// TODO: assert that client certificate used is not revoked
|
||||
|
||||
const kmipClient = await server.store.kmipClient.findByProjectAndClientId(projectId, clientId);
|
||||
|
||||
if (!kmipClient) {
|
||||
throw new NotFoundError({
|
||||
message: "KMIP client cannot be found."
|
||||
});
|
||||
}
|
||||
|
||||
if (kmipClient.orgId !== req.permission.orgId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Client specified in the request does not belong in the organization"
|
||||
});
|
||||
}
|
||||
|
||||
req.kmipUser = {
|
||||
projectId,
|
||||
clientId,
|
||||
name: kmipClient.name
|
||||
};
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/create",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "KMIP endpoint for creating managed objects",
|
||||
body: z.object({
|
||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
||||
}),
|
||||
response: {
|
||||
200: KmsKeysSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const object = await server.services.kmipOperation.create({
|
||||
...req.kmipUser,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
algorithm: req.body.algorithm
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.kmipUser.projectId,
|
||||
actor: {
|
||||
type: ActorType.KMIP_CLIENT,
|
||||
metadata: {
|
||||
clientId: req.kmipUser.clientId,
|
||||
name: req.kmipUser.name
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.KMIP_OPERATION_CREATE,
|
||||
metadata: {
|
||||
id: object.id,
|
||||
algorithm: req.body.algorithm
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return object;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/get",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "KMIP endpoint for getting managed objects",
|
||||
body: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string(),
|
||||
value: z.string(),
|
||||
algorithm: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const object = await server.services.kmipOperation.get({
|
||||
...req.kmipUser,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.body.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.kmipUser.projectId,
|
||||
actor: {
|
||||
type: ActorType.KMIP_CLIENT,
|
||||
metadata: {
|
||||
clientId: req.kmipUser.clientId,
|
||||
name: req.kmipUser.name
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.KMIP_OPERATION_GET,
|
||||
metadata: {
|
||||
id: object.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return object;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/get-attributes",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "KMIP endpoint for getting attributes of managed object",
|
||||
body: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string(),
|
||||
algorithm: z.string(),
|
||||
isActive: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const object = await server.services.kmipOperation.getAttributes({
|
||||
...req.kmipUser,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.body.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.kmipUser.projectId,
|
||||
actor: {
|
||||
type: ActorType.KMIP_CLIENT,
|
||||
metadata: {
|
||||
clientId: req.kmipUser.clientId,
|
||||
name: req.kmipUser.name
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.KMIP_OPERATION_GET_ATTRIBUTES,
|
||||
metadata: {
|
||||
id: object.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return object;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/destroy",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "KMIP endpoint for destroying managed objects",
|
||||
body: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const object = await server.services.kmipOperation.destroy({
|
||||
...req.kmipUser,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.body.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.kmipUser.projectId,
|
||||
actor: {
|
||||
type: ActorType.KMIP_CLIENT,
|
||||
metadata: {
|
||||
clientId: req.kmipUser.clientId,
|
||||
name: req.kmipUser.name
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.KMIP_OPERATION_DESTROY,
|
||||
metadata: {
|
||||
id: object.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return object;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/activate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "KMIP endpoint for activating managed object",
|
||||
body: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string(),
|
||||
isActive: z.boolean()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const object = await server.services.kmipOperation.activate({
|
||||
...req.kmipUser,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.body.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.kmipUser.projectId,
|
||||
actor: {
|
||||
type: ActorType.KMIP_CLIENT,
|
||||
metadata: {
|
||||
clientId: req.kmipUser.clientId,
|
||||
name: req.kmipUser.name
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.KMIP_OPERATION_ACTIVATE,
|
||||
metadata: {
|
||||
id: object.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return object;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/revoke",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "KMIP endpoint for revoking managed object",
|
||||
body: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const object = await server.services.kmipOperation.revoke({
|
||||
...req.kmipUser,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.body.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.kmipUser.projectId,
|
||||
actor: {
|
||||
type: ActorType.KMIP_CLIENT,
|
||||
metadata: {
|
||||
clientId: req.kmipUser.clientId,
|
||||
name: req.kmipUser.name
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.KMIP_OPERATION_REVOKE,
|
||||
metadata: {
|
||||
id: object.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return object;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/locate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "KMIP endpoint for locating managed objects",
|
||||
response: {
|
||||
200: z.object({
|
||||
objects: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
isActive: z.boolean(),
|
||||
algorithm: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const objects = await server.services.kmipOperation.locate({
|
||||
...req.kmipUser,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.kmipUser.projectId,
|
||||
actor: {
|
||||
type: ActorType.KMIP_CLIENT,
|
||||
metadata: {
|
||||
clientId: req.kmipUser.clientId,
|
||||
name: req.kmipUser.name
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.KMIP_OPERATION_LOCATE,
|
||||
metadata: {
|
||||
ids: objects.map((obj) => obj.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
objects
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/register",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "KMIP endpoint for registering managed object",
|
||||
body: z.object({
|
||||
key: z.string(),
|
||||
name: z.string(),
|
||||
algorithm: z.nativeEnum(SymmetricEncryption)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const object = await server.services.kmipOperation.register({
|
||||
...req.kmipUser,
|
||||
...req.body,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.kmipUser.projectId,
|
||||
actor: {
|
||||
type: ActorType.KMIP_CLIENT,
|
||||
metadata: {
|
||||
clientId: req.kmipUser.clientId,
|
||||
name: req.kmipUser.name
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.KMIP_OPERATION_REGISTER,
|
||||
metadata: {
|
||||
id: object.id,
|
||||
algorithm: req.body.algorithm,
|
||||
name: object.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return object;
|
||||
}
|
||||
});
|
||||
};
|
@ -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()
|
||||
})
|
||||
|
@ -21,6 +21,8 @@ import {
|
||||
TUpdateSecretSyncDTO
|
||||
} from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { KmipPermission } from "../kmip/kmip-enum";
|
||||
|
||||
export type TListProjectAuditLogDTO = {
|
||||
filter: {
|
||||
userAgentType?: UserAgentType;
|
||||
@ -39,7 +41,14 @@ export type TListProjectAuditLogDTO = {
|
||||
|
||||
export type TCreateAuditLogDTO = {
|
||||
event: Event;
|
||||
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor | UnknownUserActor;
|
||||
actor:
|
||||
| UserActor
|
||||
| IdentityActor
|
||||
| ServiceActor
|
||||
| ScimClientActor
|
||||
| PlatformActor
|
||||
| UnknownUserActor
|
||||
| KmipClientActor;
|
||||
orgId?: string;
|
||||
projectId?: string;
|
||||
} & BaseAuthData;
|
||||
@ -241,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",
|
||||
@ -252,7 +262,26 @@ export enum EventType {
|
||||
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
|
||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
||||
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
|
||||
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user"
|
||||
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user",
|
||||
CREATE_KMIP_CLIENT = "create-kmip-client",
|
||||
UPDATE_KMIP_CLIENT = "update-kmip-client",
|
||||
DELETE_KMIP_CLIENT = "delete-kmip-client",
|
||||
GET_KMIP_CLIENT = "get-kmip-client",
|
||||
GET_KMIP_CLIENTS = "get-kmip-clients",
|
||||
CREATE_KMIP_CLIENT_CERTIFICATE = "create-kmip-client-certificate",
|
||||
|
||||
SETUP_KMIP = "setup-kmip",
|
||||
GET_KMIP = "get-kmip",
|
||||
REGISTER_KMIP_SERVER = "register-kmip-server",
|
||||
|
||||
KMIP_OPERATION_CREATE = "kmip-operation-create",
|
||||
KMIP_OPERATION_GET = "kmip-operation-get",
|
||||
KMIP_OPERATION_DESTROY = "kmip-operation-destroy",
|
||||
KMIP_OPERATION_GET_ATTRIBUTES = "kmip-operation-get-attributes",
|
||||
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
|
||||
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
|
||||
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
|
||||
KMIP_OPERATION_REGISTER = "kmip-operation-register"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@ -275,6 +304,11 @@ interface ScimClientActorMetadata {}
|
||||
|
||||
interface PlatformActorMetadata {}
|
||||
|
||||
interface KmipClientActorMetadata {
|
||||
clientId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface UnknownUserActorMetadata {}
|
||||
|
||||
export interface UserActor {
|
||||
@ -292,6 +326,11 @@ export interface PlatformActor {
|
||||
metadata: PlatformActorMetadata;
|
||||
}
|
||||
|
||||
export interface KmipClientActor {
|
||||
type: ActorType.KMIP_CLIENT;
|
||||
metadata: KmipClientActorMetadata;
|
||||
}
|
||||
|
||||
export interface UnknownUserActor {
|
||||
type: ActorType.UNKNOWN_USER;
|
||||
metadata: UnknownUserActorMetadata;
|
||||
@ -307,7 +346,7 @@ export interface ScimClientActor {
|
||||
metadata: ScimClientActorMetadata;
|
||||
}
|
||||
|
||||
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor;
|
||||
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor | KmipClientActor;
|
||||
|
||||
interface GetSecretsEvent {
|
||||
type: EventType.GET_SECRETS;
|
||||
@ -352,6 +391,7 @@ interface CreateSecretBatchEvent {
|
||||
secrets: Array<{
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretPath?: string;
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
}>;
|
||||
@ -374,8 +414,14 @@ interface UpdateSecretBatchEvent {
|
||||
type: EventType.UPDATE_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number; secretMetadata?: TSecretMetadata }>;
|
||||
secretPath?: string;
|
||||
secrets: Array<{
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
secretPath?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1096,6 +1142,7 @@ interface CreateFolderEvent {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1975,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: {
|
||||
@ -2084,6 +2140,139 @@ interface OidcGroupMembershipMappingRemoveUserEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateKmipClientEvent {
|
||||
type: EventType.CREATE_KMIP_CLIENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
id: string;
|
||||
permissions: KmipPermission[];
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateKmipClientEvent {
|
||||
type: EventType.UPDATE_KMIP_CLIENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
id: string;
|
||||
permissions: KmipPermission[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteKmipClientEvent {
|
||||
type: EventType.DELETE_KMIP_CLIENT;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetKmipClientEvent {
|
||||
type: EventType.GET_KMIP_CLIENT;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetKmipClientsEvent {
|
||||
type: EventType.GET_KMIP_CLIENTS;
|
||||
metadata: {
|
||||
ids: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateKmipClientCertificateEvent {
|
||||
type: EventType.CREATE_KMIP_CLIENT_CERTIFICATE;
|
||||
metadata: {
|
||||
clientId: string;
|
||||
ttl: string;
|
||||
keyAlgorithm: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface KmipOperationGetEvent {
|
||||
type: EventType.KMIP_OPERATION_GET;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface KmipOperationDestroyEvent {
|
||||
type: EventType.KMIP_OPERATION_DESTROY;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface KmipOperationCreateEvent {
|
||||
type: EventType.KMIP_OPERATION_CREATE;
|
||||
metadata: {
|
||||
id: string;
|
||||
algorithm: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface KmipOperationGetAttributesEvent {
|
||||
type: EventType.KMIP_OPERATION_GET_ATTRIBUTES;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface KmipOperationActivateEvent {
|
||||
type: EventType.KMIP_OPERATION_ACTIVATE;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface KmipOperationRevokeEvent {
|
||||
type: EventType.KMIP_OPERATION_REVOKE;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface KmipOperationLocateEvent {
|
||||
type: EventType.KMIP_OPERATION_LOCATE;
|
||||
metadata: {
|
||||
ids: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface KmipOperationRegisterEvent {
|
||||
type: EventType.KMIP_OPERATION_REGISTER;
|
||||
metadata: {
|
||||
id: string;
|
||||
algorithm: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SetupKmipEvent {
|
||||
type: EventType.SETUP_KMIP;
|
||||
metadata: {
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetKmipEvent {
|
||||
type: EventType.GET_KMIP;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RegisterKmipServerEvent {
|
||||
type: EventType.REGISTER_KMIP_SERVER;
|
||||
metadata: {
|
||||
serverCertificateSerialNumber: string;
|
||||
hostnamesOrIps: string;
|
||||
commonName: string;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
ttl: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -2275,4 +2464,22 @@ export type Event =
|
||||
| SecretSyncImportSecretsEvent
|
||||
| SecretSyncRemoveSecretsEvent
|
||||
| OidcGroupMembershipMappingAssignUserEvent
|
||||
| OidcGroupMembershipMappingRemoveUserEvent;
|
||||
| OidcGroupMembershipMappingRemoveUserEvent
|
||||
| CreateKmipClientEvent
|
||||
| UpdateKmipClientEvent
|
||||
| DeleteKmipClientEvent
|
||||
| GetKmipClientEvent
|
||||
| GetKmipClientsEvent
|
||||
| CreateKmipClientCertificateEvent
|
||||
| SetupKmipEvent
|
||||
| GetKmipEvent
|
||||
| RegisterKmipServerEvent
|
||||
| KmipOperationGetEvent
|
||||
| KmipOperationDestroyEvent
|
||||
| KmipOperationCreateEvent
|
||||
| KmipOperationGetAttributesEvent
|
||||
| KmipOperationActivateEvent
|
||||
| KmipOperationRevokeEvent
|
||||
| KmipOperationLocateEvent
|
||||
| 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;
|
||||
};
|
11
backend/src/ee/services/kmip/kmip-client-certificate-dal.ts
Normal file
11
backend/src/ee/services/kmip/kmip-client-certificate-dal.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TKmipClientCertificateDALFactory = ReturnType<typeof kmipClientCertificateDALFactory>;
|
||||
|
||||
export const kmipClientCertificateDALFactory = (db: TDbClient) => {
|
||||
const kmipClientCertOrm = ormify(db, TableName.KmipClientCertificates);
|
||||
|
||||
return kmipClientCertOrm;
|
||||
};
|
86
backend/src/ee/services/kmip/kmip-client-dal.ts
Normal file
86
backend/src/ee/services/kmip/kmip-client-dal.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TKmipClients } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
|
||||
import { KmipClientOrderBy } from "./kmip-types";
|
||||
|
||||
export type TKmipClientDALFactory = ReturnType<typeof kmipClientDALFactory>;
|
||||
|
||||
export const kmipClientDALFactory = (db: TDbClient) => {
|
||||
const kmipClientOrm = ormify(db, TableName.KmipClient);
|
||||
|
||||
const findByProjectAndClientId = async (projectId: string, clientId: string) => {
|
||||
try {
|
||||
const client = await db
|
||||
.replicaNode()(TableName.KmipClient)
|
||||
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.KmipClient}.projectId`)
|
||||
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`)
|
||||
.where({
|
||||
[`${TableName.KmipClient}.projectId` as "projectId"]: projectId,
|
||||
[`${TableName.KmipClient}.id` as "id"]: clientId
|
||||
})
|
||||
.select(selectAllTableCols(TableName.KmipClient))
|
||||
.select(db.ref("id").withSchema(TableName.Organization).as("orgId"))
|
||||
.first();
|
||||
|
||||
return client;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by project and client ID" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByProjectId = async (
|
||||
{
|
||||
projectId,
|
||||
offset = 0,
|
||||
limit,
|
||||
orderBy = KmipClientOrderBy.Name,
|
||||
orderDirection = OrderByDirection.ASC,
|
||||
search
|
||||
}: {
|
||||
projectId: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: KmipClientOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const query = (tx || db.replicaNode())(TableName.KmipClient)
|
||||
.where("projectId", projectId)
|
||||
.where((qb) => {
|
||||
if (search) {
|
||||
void qb.whereILike("name", `%${search}%`);
|
||||
}
|
||||
})
|
||||
.select<
|
||||
(TKmipClients & {
|
||||
total_count: number;
|
||||
})[]
|
||||
>(selectAllTableCols(TableName.KmipClient), db.raw(`count(*) OVER() as total_count`))
|
||||
.orderBy(orderBy, orderDirection);
|
||||
|
||||
if (limit) {
|
||||
void query.limit(limit).offset(offset);
|
||||
}
|
||||
|
||||
const data = await query;
|
||||
|
||||
return { kmipClients: data, totalCount: Number(data?.[0]?.total_count ?? 0) };
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find KMIP clients by project id" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...kmipClientOrm,
|
||||
findByProjectId,
|
||||
findByProjectAndClientId
|
||||
};
|
||||
};
|
11
backend/src/ee/services/kmip/kmip-enum.ts
Normal file
11
backend/src/ee/services/kmip/kmip-enum.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export enum KmipPermission {
|
||||
Create = "create",
|
||||
Locate = "locate",
|
||||
Check = "check",
|
||||
Get = "get",
|
||||
GetAttributes = "get-attributes",
|
||||
Activate = "activate",
|
||||
Revoke = "revoke",
|
||||
Destroy = "destroy",
|
||||
Register = "register"
|
||||
}
|
422
backend/src/ee/services/kmip/kmip-operation-service.ts
Normal file
422
backend/src/ee/services/kmip/kmip-operation-service.ts
Normal file
@ -0,0 +1,422 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TKmipClientDALFactory } from "./kmip-client-dal";
|
||||
import { KmipPermission } from "./kmip-enum";
|
||||
import {
|
||||
TKmipCreateDTO,
|
||||
TKmipDestroyDTO,
|
||||
TKmipGetAttributesDTO,
|
||||
TKmipGetDTO,
|
||||
TKmipLocateDTO,
|
||||
TKmipRegisterDTO,
|
||||
TKmipRevokeDTO
|
||||
} from "./kmip-types";
|
||||
|
||||
type TKmipOperationServiceFactoryDep = {
|
||||
kmsService: TKmsServiceFactory;
|
||||
kmsDAL: TKmsKeyDALFactory;
|
||||
kmipClientDAL: TKmipClientDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "getProjectFromSplitId" | "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
};
|
||||
|
||||
export type TKmipOperationServiceFactory = ReturnType<typeof kmipOperationServiceFactory>;
|
||||
|
||||
export const kmipOperationServiceFactory = ({
|
||||
kmsService,
|
||||
kmsDAL,
|
||||
projectDAL,
|
||||
kmipClientDAL,
|
||||
permissionService
|
||||
}: TKmipOperationServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
projectId,
|
||||
clientId,
|
||||
algorithm,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TKmipCreateDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
const kmipClient = await kmipClientDAL.findOne({
|
||||
id: clientId,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!kmipClient.permissions?.includes(KmipPermission.Create)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Client does not have sufficient permission to perform KMIP create"
|
||||
});
|
||||
}
|
||||
|
||||
const kmsKey = await kmsService.generateKmsKey({
|
||||
encryptionAlgorithm: algorithm,
|
||||
orgId: actorOrgId,
|
||||
projectId,
|
||||
isReserved: false
|
||||
});
|
||||
|
||||
return kmsKey;
|
||||
};
|
||||
|
||||
const destroy = async ({ projectId, id, clientId, actor, actorId, actorOrgId, actorAuthMethod }: TKmipDestroyDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
const kmipClient = await kmipClientDAL.findOne({
|
||||
id: clientId,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!kmipClient.permissions?.includes(KmipPermission.Destroy)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Client does not have sufficient permission to perform KMIP destroy"
|
||||
});
|
||||
}
|
||||
|
||||
const key = await kmsDAL.findOne({
|
||||
id,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!key) {
|
||||
throw new NotFoundError({ message: `Key with ID ${id} not found` });
|
||||
}
|
||||
|
||||
if (key.isReserved) {
|
||||
throw new BadRequestError({ message: "Cannot destroy reserved keys" });
|
||||
}
|
||||
|
||||
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
|
||||
if (!completeKeyDetails.internalKms) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot destroy external keys"
|
||||
});
|
||||
}
|
||||
|
||||
if (!completeKeyDetails.isDisabled) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot destroy active keys"
|
||||
});
|
||||
}
|
||||
|
||||
const kms = kmsDAL.deleteById(id);
|
||||
|
||||
return kms;
|
||||
};
|
||||
|
||||
const get = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
const kmipClient = await kmipClientDAL.findOne({
|
||||
id: clientId,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!kmipClient.permissions?.includes(KmipPermission.Get)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Client does not have sufficient permission to perform KMIP get"
|
||||
});
|
||||
}
|
||||
|
||||
const key = await kmsDAL.findOne({
|
||||
id,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!key) {
|
||||
throw new NotFoundError({ message: `Key with ID ${id} not found` });
|
||||
}
|
||||
|
||||
if (key.isReserved) {
|
||||
throw new BadRequestError({ message: "Cannot get reserved keys" });
|
||||
}
|
||||
|
||||
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
|
||||
|
||||
if (!completeKeyDetails.internalKms) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot get external keys"
|
||||
});
|
||||
}
|
||||
|
||||
const kmsKey = await kmsService.getKeyMaterial({
|
||||
kmsId: key.id
|
||||
});
|
||||
|
||||
return {
|
||||
id: key.id,
|
||||
value: kmsKey.toString("base64"),
|
||||
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
|
||||
isActive: !key.isDisabled,
|
||||
createdAt: key.createdAt,
|
||||
updatedAt: key.updatedAt
|
||||
};
|
||||
};
|
||||
|
||||
const activate = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipGetDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
const kmipClient = await kmipClientDAL.findOne({
|
||||
id: clientId,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!kmipClient.permissions?.includes(KmipPermission.Activate)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Client does not have sufficient permission to perform KMIP activate"
|
||||
});
|
||||
}
|
||||
|
||||
const key = await kmsDAL.findOne({
|
||||
id,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!key) {
|
||||
throw new NotFoundError({ message: `Key with ID ${id} not found` });
|
||||
}
|
||||
|
||||
return {
|
||||
id: key.id,
|
||||
isActive: !key.isDisabled
|
||||
};
|
||||
};
|
||||
|
||||
const revoke = async ({ projectId, id, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipRevokeDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
const kmipClient = await kmipClientDAL.findOne({
|
||||
id: clientId,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!kmipClient.permissions?.includes(KmipPermission.Revoke)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Client does not have sufficient permission to perform KMIP revoke"
|
||||
});
|
||||
}
|
||||
|
||||
const key = await kmsDAL.findOne({
|
||||
id,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!key) {
|
||||
throw new NotFoundError({ message: `Key with ID ${id} not found` });
|
||||
}
|
||||
|
||||
if (key.isReserved) {
|
||||
throw new BadRequestError({ message: "Cannot revoke reserved keys" });
|
||||
}
|
||||
|
||||
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
|
||||
|
||||
if (!completeKeyDetails.internalKms) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot revoke external keys"
|
||||
});
|
||||
}
|
||||
|
||||
const revokedKey = await kmsDAL.updateById(key.id, {
|
||||
isDisabled: true
|
||||
});
|
||||
|
||||
return {
|
||||
id: key.id,
|
||||
updatedAt: revokedKey.updatedAt
|
||||
};
|
||||
};
|
||||
|
||||
const getAttributes = async ({
|
||||
projectId,
|
||||
id,
|
||||
clientId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TKmipGetAttributesDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
const kmipClient = await kmipClientDAL.findOne({
|
||||
id: clientId,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!kmipClient.permissions?.includes(KmipPermission.GetAttributes)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Client does not have sufficient permission to perform KMIP get attributes"
|
||||
});
|
||||
}
|
||||
|
||||
const key = await kmsDAL.findOne({
|
||||
id,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!key) {
|
||||
throw new NotFoundError({ message: `Key with ID ${id} not found` });
|
||||
}
|
||||
|
||||
if (key.isReserved) {
|
||||
throw new BadRequestError({ message: "Cannot get reserved keys" });
|
||||
}
|
||||
|
||||
const completeKeyDetails = await kmsDAL.findByIdWithAssociatedKms(id);
|
||||
|
||||
if (!completeKeyDetails.internalKms) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot get external keys"
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: key.id,
|
||||
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
|
||||
isActive: !key.isDisabled,
|
||||
createdAt: key.createdAt,
|
||||
updatedAt: key.updatedAt
|
||||
};
|
||||
};
|
||||
|
||||
const locate = async ({ projectId, clientId, actor, actorId, actorAuthMethod, actorOrgId }: TKmipLocateDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
const kmipClient = await kmipClientDAL.findOne({
|
||||
id: clientId,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!kmipClient.permissions?.includes(KmipPermission.Locate)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Client does not have sufficient permission to perform KMIP locate"
|
||||
});
|
||||
}
|
||||
|
||||
const keys = await kmsDAL.findProjectCmeks(projectId);
|
||||
|
||||
return keys;
|
||||
};
|
||||
|
||||
const register = async ({
|
||||
projectId,
|
||||
clientId,
|
||||
key,
|
||||
algorithm,
|
||||
name,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TKmipRegisterDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
const kmipClient = await kmipClientDAL.findOne({
|
||||
id: clientId,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!kmipClient.permissions?.includes(KmipPermission.Register)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Client does not have sufficient permission to perform KMIP register"
|
||||
});
|
||||
}
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
|
||||
const kmsKey = await kmsService.importKeyMaterial({
|
||||
name,
|
||||
key: Buffer.from(key, "base64"),
|
||||
algorithm,
|
||||
isReserved: false,
|
||||
projectId,
|
||||
orgId: project.orgId
|
||||
});
|
||||
|
||||
return kmsKey;
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
get,
|
||||
activate,
|
||||
getAttributes,
|
||||
destroy,
|
||||
revoke,
|
||||
locate,
|
||||
register
|
||||
};
|
||||
};
|
11
backend/src/ee/services/kmip/kmip-org-config-dal.ts
Normal file
11
backend/src/ee/services/kmip/kmip-org-config-dal.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TKmipOrgConfigDALFactory = ReturnType<typeof kmipOrgConfigDALFactory>;
|
||||
|
||||
export const kmipOrgConfigDALFactory = (db: TDbClient) => {
|
||||
const kmipOrgConfigOrm = ormify(db, TableName.KmipOrgConfig);
|
||||
|
||||
return kmipOrgConfigOrm;
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TKmipOrgServerCertificateDALFactory = ReturnType<typeof kmipOrgServerCertificateDALFactory>;
|
||||
|
||||
export const kmipOrgServerCertificateDALFactory = (db: TDbClient) => {
|
||||
const kmipOrgServerCertificateOrm = ormify(db, TableName.KmipOrgServerCertificates);
|
||||
|
||||
return kmipOrgServerCertificateOrm;
|
||||
};
|
817
backend/src/ee/services/kmip/kmip-service.ts
Normal file
817
backend/src/ee/services/kmip/kmip-service.ts
Normal file
@ -0,0 +1,817 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import crypto, { KeyObject } from "crypto";
|
||||
import ms from "ms";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { isValidHostname, isValidIp } from "@app/lib/ip";
|
||||
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
|
||||
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 { OrgPermissionKmipActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionKmipActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TKmipClientCertificateDALFactory } from "./kmip-client-certificate-dal";
|
||||
import { TKmipClientDALFactory } from "./kmip-client-dal";
|
||||
import { TKmipOrgConfigDALFactory } from "./kmip-org-config-dal";
|
||||
import { TKmipOrgServerCertificateDALFactory } from "./kmip-org-server-certificate-dal";
|
||||
import {
|
||||
TCreateKmipClientCertificateDTO,
|
||||
TCreateKmipClientDTO,
|
||||
TDeleteKmipClientDTO,
|
||||
TGenerateOrgKmipServerCertificateDTO,
|
||||
TGetKmipClientDTO,
|
||||
TGetOrgKmipDTO,
|
||||
TListKmipClientsByProjectIdDTO,
|
||||
TRegisterServerDTO,
|
||||
TSetupOrgKmipDTO,
|
||||
TUpdateKmipClientDTO
|
||||
} from "./kmip-types";
|
||||
|
||||
type TKmipServiceFactoryDep = {
|
||||
kmipClientDAL: TKmipClientDALFactory;
|
||||
kmipClientCertificateDAL: TKmipClientCertificateDALFactory;
|
||||
kmipOrgServerCertificateDAL: TKmipOrgServerCertificateDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
kmipOrgConfigDAL: TKmipOrgConfigDALFactory;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TKmipServiceFactory = ReturnType<typeof kmipServiceFactory>;
|
||||
|
||||
export const kmipServiceFactory = ({
|
||||
kmipClientDAL,
|
||||
permissionService,
|
||||
kmipClientCertificateDAL,
|
||||
kmipOrgConfigDAL,
|
||||
kmsService,
|
||||
kmipOrgServerCertificateDAL,
|
||||
licenseService
|
||||
}: TKmipServiceFactoryDep) => {
|
||||
const createKmipClient = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
name,
|
||||
description,
|
||||
permissions
|
||||
}: TCreateKmipClientDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionKmipActions.CreateClients,
|
||||
ProjectPermissionSub.Kmip
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.kmip)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create KMIP client. Upgrade your plan to enterprise."
|
||||
});
|
||||
|
||||
const kmipClient = await kmipClientDAL.create({
|
||||
projectId,
|
||||
name,
|
||||
description,
|
||||
permissions
|
||||
});
|
||||
|
||||
return kmipClient;
|
||||
};
|
||||
|
||||
const updateKmipClient = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
name,
|
||||
description,
|
||||
permissions,
|
||||
id
|
||||
}: TUpdateKmipClientDTO) => {
|
||||
const kmipClient = await kmipClientDAL.findById(id);
|
||||
|
||||
if (!kmipClient) {
|
||||
throw new NotFoundError({
|
||||
message: `KMIP client with ID ${id} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.kmip)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update KMIP client. Upgrade your plan to enterprise."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: kmipClient.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionKmipActions.UpdateClients,
|
||||
ProjectPermissionSub.Kmip
|
||||
);
|
||||
|
||||
const updatedKmipClient = await kmipClientDAL.updateById(id, {
|
||||
name,
|
||||
description,
|
||||
permissions
|
||||
});
|
||||
|
||||
return updatedKmipClient;
|
||||
};
|
||||
|
||||
const deleteKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteKmipClientDTO) => {
|
||||
const kmipClient = await kmipClientDAL.findById(id);
|
||||
|
||||
if (!kmipClient) {
|
||||
throw new NotFoundError({
|
||||
message: `KMIP client with ID ${id} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: kmipClient.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionKmipActions.DeleteClients,
|
||||
ProjectPermissionSub.Kmip
|
||||
);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.kmip)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to delete KMIP client. Upgrade your plan to enterprise."
|
||||
});
|
||||
|
||||
const deletedKmipClient = await kmipClientDAL.deleteById(id);
|
||||
|
||||
return deletedKmipClient;
|
||||
};
|
||||
|
||||
const getKmipClient = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetKmipClientDTO) => {
|
||||
const kmipClient = await kmipClientDAL.findById(id);
|
||||
|
||||
if (!kmipClient) {
|
||||
throw new NotFoundError({
|
||||
message: `KMIP client with ID ${id} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: kmipClient.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip);
|
||||
|
||||
return kmipClient;
|
||||
};
|
||||
|
||||
const listKmipClientsByProjectId = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
...rest
|
||||
}: TListKmipClientsByProjectIdDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionKmipActions.ReadClients, ProjectPermissionSub.Kmip);
|
||||
|
||||
return kmipClientDAL.findByProjectId({ projectId, ...rest });
|
||||
};
|
||||
|
||||
const createKmipClientCertificate = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
ttl,
|
||||
keyAlgorithm,
|
||||
clientId
|
||||
}: TCreateKmipClientCertificateDTO) => {
|
||||
const kmipClient = await kmipClientDAL.findById(clientId);
|
||||
|
||||
if (!kmipClient) {
|
||||
throw new NotFoundError({
|
||||
message: `KMIP client with ID ${clientId} does not exist`
|
||||
});
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.kmip)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create KMIP client. Upgrade your plan to enterprise."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: kmipClient.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionKmipActions.GenerateClientCertificates,
|
||||
ProjectPermissionSub.Kmip
|
||||
);
|
||||
|
||||
const kmipConfig = await kmipOrgConfigDAL.findOne({
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
if (!kmipConfig) {
|
||||
throw new InternalServerError({
|
||||
message: "KMIP has not been configured for the organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const caCertObj = new x509.X509Certificate(
|
||||
decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate })
|
||||
);
|
||||
|
||||
const notBeforeDate = new Date();
|
||||
const notAfterDate = new Date(new Date().getTime() + ms(ttl));
|
||||
|
||||
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||
|
||||
// check not before constraint
|
||||
if (notBeforeDate < caCertNotBeforeDate) {
|
||||
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||
}
|
||||
|
||||
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
|
||||
|
||||
// check not after constraint
|
||||
if (notAfterDate > caCertNotAfterDate) {
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(keyAlgorithm);
|
||||
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(leafKeys.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 caAlg = keyAlgorithmToAlgCfg(kmipConfig.caKeyAlgorithm as CertKeyAlgorithm);
|
||||
|
||||
const caSkObj = crypto.createPrivateKey({
|
||||
key: decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaPrivateKey }),
|
||||
format: "der",
|
||||
type: "pkcs8"
|
||||
});
|
||||
|
||||
const caPrivateKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
caSkObj.export({ format: "der", type: "pkcs8" }),
|
||||
caAlg,
|
||||
true,
|
||||
["sign"]
|
||||
);
|
||||
|
||||
const serialNumber = createSerialNumber();
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: `OU=${kmipClient.projectId},CN=${clientId}`,
|
||||
issuer: caCertObj.subject,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
signingKey: caPrivateKey,
|
||||
publicKey: leafKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions
|
||||
});
|
||||
|
||||
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||
|
||||
const rootCaCert = new x509.X509Certificate(decryptor({ cipherTextBlob: kmipConfig.encryptedRootCaCertificate }));
|
||||
const serverIntermediateCaCert = new x509.X509Certificate(
|
||||
decryptor({ cipherTextBlob: kmipConfig.encryptedServerIntermediateCaCertificate })
|
||||
);
|
||||
|
||||
await kmipClientCertificateDAL.create({
|
||||
kmipClientId: clientId,
|
||||
keyAlgorithm,
|
||||
issuedAt: notBeforeDate,
|
||||
expiration: notAfterDate,
|
||||
serialNumber
|
||||
});
|
||||
|
||||
return {
|
||||
serialNumber,
|
||||
privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string,
|
||||
certificate: leafCert.toString("pem"),
|
||||
certificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]),
|
||||
projectId: kmipClient.projectId
|
||||
};
|
||||
};
|
||||
|
||||
const getServerCertificateBySerialNumber = async (orgId: string, serialNumber: string) => {
|
||||
const serverCert = await kmipOrgServerCertificateDAL.findOne({
|
||||
serialNumber,
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!serverCert) {
|
||||
throw new NotFoundError({
|
||||
message: "Server certificate not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
|
||||
const parsedCertificate = new x509.X509Certificate(decryptor({ cipherTextBlob: serverCert.encryptedCertificate }));
|
||||
|
||||
return {
|
||||
publicKey: parsedCertificate.publicKey.toString("pem"),
|
||||
keyAlgorithm: serverCert.keyAlgorithm as CertKeyAlgorithm
|
||||
};
|
||||
};
|
||||
|
||||
const setupOrgKmip = async ({ caKeyAlgorithm, actorOrgId, actor, actorId, actorAuthMethod }: TSetupOrgKmipDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
|
||||
|
||||
const kmipConfig = await kmipOrgConfigDAL.findOne({
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
if (kmipConfig) {
|
||||
throw new BadRequestError({
|
||||
message: "KMIP has already been configured for the organization"
|
||||
});
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.kmip)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to setup KMIP. Upgrade your plan to enterprise."
|
||||
});
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(caKeyAlgorithm);
|
||||
|
||||
// generate root CA
|
||||
const rootCaSerialNumber = createSerialNumber();
|
||||
const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const rootCaSkObj = KeyObject.from(rootCaKeys.privateKey);
|
||||
const rootCaIssuedAt = new Date();
|
||||
const rootCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 20));
|
||||
|
||||
const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||
name: `CN=KMIP Root CA,OU=${actorOrgId}`,
|
||||
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 intermediate server CA
|
||||
const serverIntermediateCaSerialNumber = createSerialNumber();
|
||||
const serverIntermediateCaIssuedAt = new Date();
|
||||
const serverIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
|
||||
const serverIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const serverIntermediateCaSkObj = KeyObject.from(serverIntermediateCaKeys.privateKey);
|
||||
|
||||
const serverIntermediateCaCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber: serverIntermediateCaSerialNumber,
|
||||
subject: `CN=KMIP Server Intermediate CA,OU=${actorOrgId}`,
|
||||
issuer: rootCaCert.subject,
|
||||
notBefore: serverIntermediateCaIssuedAt,
|
||||
notAfter: serverIntermediateCaExpiration,
|
||||
signingKey: rootCaKeys.privateKey,
|
||||
publicKey: serverIntermediateCaKeys.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(serverIntermediateCaKeys.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
// generate intermediate client CA
|
||||
const clientIntermediateCaSerialNumber = createSerialNumber();
|
||||
const clientIntermediateCaIssuedAt = new Date();
|
||||
const clientIntermediateCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 10));
|
||||
const clientIntermediateCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const clientIntermediateCaSkObj = KeyObject.from(clientIntermediateCaKeys.privateKey);
|
||||
|
||||
const clientIntermediateCaCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber: clientIntermediateCaSerialNumber,
|
||||
subject: `CN=KMIP Client Intermediate CA,OU=${actorOrgId}`,
|
||||
issuer: rootCaCert.subject,
|
||||
notBefore: clientIntermediateCaIssuedAt,
|
||||
notAfter: clientIntermediateCaExpiration,
|
||||
signingKey: rootCaKeys.privateKey,
|
||||
publicKey: clientIntermediateCaKeys.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(clientIntermediateCaKeys.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
await kmipOrgConfigDAL.create({
|
||||
orgId: actorOrgId,
|
||||
caKeyAlgorithm,
|
||||
rootCaIssuedAt,
|
||||
rootCaExpiration,
|
||||
rootCaSerialNumber,
|
||||
encryptedRootCaCertificate: encryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob,
|
||||
encryptedRootCaPrivateKey: encryptor({
|
||||
plainText: rootCaSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
serverIntermediateCaIssuedAt,
|
||||
serverIntermediateCaExpiration,
|
||||
serverIntermediateCaSerialNumber,
|
||||
encryptedServerIntermediateCaCertificate: encryptor({
|
||||
plainText: Buffer.from(new Uint8Array(serverIntermediateCaCert.rawData))
|
||||
}).cipherTextBlob,
|
||||
encryptedServerIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) })
|
||||
.cipherTextBlob,
|
||||
encryptedServerIntermediateCaPrivateKey: encryptor({
|
||||
plainText: serverIntermediateCaSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
clientIntermediateCaIssuedAt,
|
||||
clientIntermediateCaExpiration,
|
||||
clientIntermediateCaSerialNumber,
|
||||
encryptedClientIntermediateCaCertificate: encryptor({
|
||||
plainText: Buffer.from(new Uint8Array(clientIntermediateCaCert.rawData))
|
||||
}).cipherTextBlob,
|
||||
encryptedClientIntermediateCaChain: encryptor({ plainText: Buffer.from(rootCaCert.toString("pem")) })
|
||||
.cipherTextBlob,
|
||||
encryptedClientIntermediateCaPrivateKey: encryptor({
|
||||
plainText: clientIntermediateCaSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob
|
||||
});
|
||||
|
||||
return {
|
||||
serverCertificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]),
|
||||
clientCertificateChain: constructPemChainFromCerts([clientIntermediateCaCert, rootCaCert])
|
||||
};
|
||||
};
|
||||
|
||||
const getOrgKmip = async ({ actorOrgId, actor, actorId, actorAuthMethod }: TGetOrgKmipDTO) => {
|
||||
await permissionService.getOrgPermission(actor, actorId, actorOrgId, actorAuthMethod, actorOrgId);
|
||||
|
||||
const kmipConfig = await kmipOrgConfigDAL.findOne({
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
if (!kmipConfig) {
|
||||
throw new BadRequestError({
|
||||
message: "KMIP has not been configured for the organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const rootCaCert = new x509.X509Certificate(decryptor({ cipherTextBlob: kmipConfig.encryptedRootCaCertificate }));
|
||||
const serverIntermediateCaCert = new x509.X509Certificate(
|
||||
decryptor({ cipherTextBlob: kmipConfig.encryptedServerIntermediateCaCertificate })
|
||||
);
|
||||
|
||||
const clientIntermediateCaCert = new x509.X509Certificate(
|
||||
decryptor({ cipherTextBlob: kmipConfig.encryptedClientIntermediateCaCertificate })
|
||||
);
|
||||
|
||||
return {
|
||||
id: kmipConfig.id,
|
||||
serverCertificateChain: constructPemChainFromCerts([serverIntermediateCaCert, rootCaCert]),
|
||||
clientCertificateChain: constructPemChainFromCerts([clientIntermediateCaCert, rootCaCert])
|
||||
};
|
||||
};
|
||||
|
||||
const generateOrgKmipServerCertificate = async ({
|
||||
orgId,
|
||||
ttl,
|
||||
commonName,
|
||||
altNames,
|
||||
keyAlgorithm
|
||||
}: TGenerateOrgKmipServerCertificateDTO) => {
|
||||
const kmipOrgConfig = await kmipOrgConfigDAL.findOne({
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!kmipOrgConfig) {
|
||||
throw new BadRequestError({
|
||||
message: "KMIP has not been configured for the organization"
|
||||
});
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.kmip)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to generate KMIP server certificate. Upgrade your plan to enterprise."
|
||||
});
|
||||
|
||||
const { decryptor, encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId
|
||||
});
|
||||
|
||||
const caCertObj = new x509.X509Certificate(
|
||||
decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaCertificate })
|
||||
);
|
||||
|
||||
const notBeforeDate = new Date();
|
||||
const notAfterDate = new Date(new Date().getTime() + ms(ttl));
|
||||
|
||||
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||
|
||||
// check not before constraint
|
||||
if (notBeforeDate < caCertNotBeforeDate) {
|
||||
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||
}
|
||||
|
||||
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
|
||||
|
||||
// check not after constraint
|
||||
if (notAfterDate > caCertNotAfterDate) {
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(keyAlgorithm);
|
||||
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(leafKeys.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)
|
||||
];
|
||||
|
||||
const altNamesArray: {
|
||||
type: "email" | "dns" | "ip";
|
||||
value: string;
|
||||
}[] = altNames
|
||||
.split(",")
|
||||
.map((name) => name.trim())
|
||||
.map((altName) => {
|
||||
if (isValidHostname(altName)) {
|
||||
return {
|
||||
type: "dns",
|
||||
value: altName
|
||||
};
|
||||
}
|
||||
|
||||
if (isValidIp(altName)) {
|
||||
return {
|
||||
type: "ip",
|
||||
value: altName
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Invalid altName: ${altName}`);
|
||||
});
|
||||
|
||||
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||
extensions.push(altNamesExtension);
|
||||
|
||||
const caAlg = keyAlgorithmToAlgCfg(kmipOrgConfig.caKeyAlgorithm as CertKeyAlgorithm);
|
||||
|
||||
const decryptedCaCertChain = decryptor({
|
||||
cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaChain
|
||||
}).toString("utf-8");
|
||||
|
||||
const caSkObj = crypto.createPrivateKey({
|
||||
key: decryptor({ cipherTextBlob: kmipOrgConfig.encryptedServerIntermediateCaPrivateKey }),
|
||||
format: "der",
|
||||
type: "pkcs8"
|
||||
});
|
||||
|
||||
const caPrivateKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
caSkObj.export({ format: "der", type: "pkcs8" }),
|
||||
caAlg,
|
||||
true,
|
||||
["sign"]
|
||||
);
|
||||
|
||||
const serialNumber = createSerialNumber();
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: `CN=${commonName}`,
|
||||
issuer: caCertObj.subject,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
signingKey: caPrivateKey,
|
||||
publicKey: leafKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions
|
||||
});
|
||||
|
||||
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||
const certificateChain = `${caCertObj.toString("pem")}\n${decryptedCaCertChain}`.trim();
|
||||
|
||||
await kmipOrgServerCertificateDAL.create({
|
||||
orgId,
|
||||
keyAlgorithm,
|
||||
issuedAt: notBeforeDate,
|
||||
expiration: notAfterDate,
|
||||
serialNumber,
|
||||
commonName,
|
||||
altNames,
|
||||
encryptedCertificate: encryptor({ plainText: Buffer.from(new Uint8Array(leafCert.rawData)) }).cipherTextBlob,
|
||||
encryptedChain: encryptor({ plainText: Buffer.from(certificateChain) }).cipherTextBlob
|
||||
});
|
||||
|
||||
return {
|
||||
serialNumber,
|
||||
privateKey: skLeafObj.export({ format: "pem", type: "pkcs8" }) as string,
|
||||
certificate: leafCert.toString("pem"),
|
||||
certificateChain
|
||||
};
|
||||
};
|
||||
|
||||
const registerServer = async ({
|
||||
actorOrgId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
ttl,
|
||||
commonName,
|
||||
keyAlgorithm,
|
||||
hostnamesOrIps
|
||||
}: TRegisterServerDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
const kmipConfig = await kmipOrgConfigDAL.findOne({
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
if (!kmipConfig) {
|
||||
throw new BadRequestError({
|
||||
message: "KMIP has not been configured for the organization"
|
||||
});
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.kmip)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to register KMIP server. Upgrade your plan to enterprise."
|
||||
});
|
||||
|
||||
const { privateKey, certificate, certificateChain, serialNumber } = await generateOrgKmipServerCertificate({
|
||||
orgId: actorOrgId,
|
||||
commonName: commonName ?? "kmip-server",
|
||||
altNames: hostnamesOrIps,
|
||||
keyAlgorithm: keyAlgorithm ?? (kmipConfig.caKeyAlgorithm as CertKeyAlgorithm),
|
||||
ttl
|
||||
});
|
||||
|
||||
const { clientCertificateChain } = await getOrgKmip({
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorId,
|
||||
actorOrgId
|
||||
});
|
||||
|
||||
return {
|
||||
serverCertificateSerialNumber: serialNumber,
|
||||
clientCertificateChain,
|
||||
privateKey,
|
||||
certificate,
|
||||
certificateChain
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createKmipClient,
|
||||
updateKmipClient,
|
||||
deleteKmipClient,
|
||||
getKmipClient,
|
||||
listKmipClientsByProjectId,
|
||||
createKmipClientCertificate,
|
||||
setupOrgKmip,
|
||||
generateOrgKmipServerCertificate,
|
||||
getOrgKmip,
|
||||
getServerCertificateBySerialNumber,
|
||||
registerServer
|
||||
};
|
||||
};
|
102
backend/src/ee/services/kmip/kmip-types.ts
Normal file
102
backend/src/ee/services/kmip/kmip-types.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { OrderByDirection, TOrgPermission, TProjectPermission } from "@app/lib/types";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
|
||||
import { KmipPermission } from "./kmip-enum";
|
||||
|
||||
export type TCreateKmipClientCertificateDTO = {
|
||||
clientId: string;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
ttl: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateKmipClientDTO = {
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions: KmipPermission[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdateKmipClientDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
permissions?: KmipPermission[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteKmipClientDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetKmipClientDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export enum KmipClientOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
||||
export type TListKmipClientsByProjectIdDTO = {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: KmipClientOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
type KmipOperationBaseDTO = {
|
||||
clientId: string;
|
||||
projectId: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TKmipCreateDTO = {
|
||||
algorithm: SymmetricEncryption;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TKmipGetDTO = {
|
||||
id: string;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TKmipGetAttributesDTO = {
|
||||
id: string;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TKmipDestroyDTO = {
|
||||
id: string;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TKmipActivateDTO = {
|
||||
id: string;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TKmipRevokeDTO = {
|
||||
id: string;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TKmipLocateDTO = KmipOperationBaseDTO;
|
||||
|
||||
export type TKmipRegisterDTO = {
|
||||
name: string;
|
||||
key: string;
|
||||
algorithm: SymmetricEncryption;
|
||||
} & KmipOperationBaseDTO;
|
||||
|
||||
export type TSetupOrgKmipDTO = {
|
||||
caKeyAlgorithm: CertKeyAlgorithm;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetOrgKmipDTO = Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGenerateOrgKmipServerCertificateDTO = {
|
||||
commonName: string;
|
||||
altNames: string;
|
||||
keyAlgorithm: CertKeyAlgorithm;
|
||||
ttl: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TRegisterServerDTO = {
|
||||
hostnamesOrIps: string;
|
||||
commonName?: string;
|
||||
keyAlgorithm?: CertKeyAlgorithm;
|
||||
ttl: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
@ -50,7 +50,9 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
},
|
||||
pkiEst: false,
|
||||
enforceMfa: false,
|
||||
projectTemplates: false
|
||||
projectTemplates: false,
|
||||
kmip: false,
|
||||
gateway: false
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
@ -68,6 +68,8 @@ export type TFeatureSet = {
|
||||
pkiEst: boolean;
|
||||
enforceMfa: boolean;
|
||||
projectTemplates: false;
|
||||
kmip: false;
|
||||
gateway: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@ -23,10 +23,23 @@ export enum OrgPermissionAppConnectionActions {
|
||||
Connect = "connect"
|
||||
}
|
||||
|
||||
export enum OrgPermissionKmipActions {
|
||||
Proxy = "proxy",
|
||||
Setup = "setup"
|
||||
}
|
||||
|
||||
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",
|
||||
@ -44,7 +57,9 @@ export enum OrgPermissionSubjects {
|
||||
AdminConsole = "organization-admin-console",
|
||||
AuditLogs = "audit-logs",
|
||||
ProjectTemplates = "project-templates",
|
||||
AppConnections = "app-connections"
|
||||
AppConnections = "app-connections",
|
||||
Kmip = "kmip",
|
||||
Gateway = "gateway"
|
||||
}
|
||||
|
||||
export type AppConnectionSubjectFields = {
|
||||
@ -67,6 +82,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
|
||||
| [
|
||||
OrgPermissionAppConnectionActions,
|
||||
(
|
||||
@ -74,7 +90,8 @@ export type OrgPermissionSet =
|
||||
| (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
|
||||
)
|
||||
]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
||||
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
|
||||
|
||||
const AppConnectionConditionSchema = z
|
||||
.object({
|
||||
@ -167,6 +184,18 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAdminConsoleAction).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(OrgPermissionSubjects.Kmip).describe("The entity this permission pertains to."),
|
||||
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."
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@ -251,8 +280,18 @@ 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);
|
||||
|
||||
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
|
||||
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@ -282,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;
|
||||
};
|
||||
|
@ -44,6 +44,14 @@ export enum ProjectPermissionSecretSyncActions {
|
||||
RemoveSecrets = "remove-secrets"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionKmipActions {
|
||||
CreateClients = "create-clients",
|
||||
UpdateClients = "update-clients",
|
||||
DeleteClients = "delete-clients",
|
||||
ReadClients = "read-clients",
|
||||
GenerateClientCertificates = "generate-client-certificates"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSub {
|
||||
Role = "role",
|
||||
Member = "member",
|
||||
@ -75,7 +83,8 @@ export enum ProjectPermissionSub {
|
||||
PkiCollections = "pki-collections",
|
||||
Kms = "kms",
|
||||
Cmek = "cmek",
|
||||
SecretSyncs = "secret-syncs"
|
||||
SecretSyncs = "secret-syncs",
|
||||
Kmip = "kmip"
|
||||
}
|
||||
|
||||
export type SecretSubjectFields = {
|
||||
@ -156,6 +165,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
||||
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
|
||||
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||
@ -410,6 +420,12 @@ const GeneralPermissionSchema = [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
})
|
||||
];
|
||||
|
||||
@ -575,6 +591,18 @@ const buildAdminPermissionRules = () => {
|
||||
],
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionKmipActions.CreateClients,
|
||||
ProjectPermissionKmipActions.UpdateClients,
|
||||
ProjectPermissionKmipActions.DeleteClients,
|
||||
ProjectPermissionKmipActions.ReadClients,
|
||||
ProjectPermissionKmipActions.GenerateClientCertificates
|
||||
],
|
||||
ProjectPermissionSub.Kmip
|
||||
);
|
||||
|
||||
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.",
|
||||
@ -721,7 +723,8 @@ export const RAW_SECRETS = {
|
||||
secretName: "The name of the secret to update.",
|
||||
secretComment: "Update comment to the secret.",
|
||||
environment: "The slug of the environment where the secret is located.",
|
||||
secretPath: "The path of the secret to update.",
|
||||
mode: "Defines how the system should handle missing secrets during an update.",
|
||||
secretPath: "The default path for secrets to update or upsert, if not provided in the secret details.",
|
||||
secretValue: "The new value of the secret.",
|
||||
skipMultilineEncoding: "Skip multiline encoding for the secret value.",
|
||||
type: "The type of the secret to update.",
|
||||
@ -1721,6 +1724,18 @@ export const SecretSyncs = {
|
||||
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
|
||||
};
|
||||
},
|
||||
ADDITIONAL_SYNC_OPTIONS: {
|
||||
AWS_PARAMETER_STORE: {
|
||||
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
||||
tags: "Optional resource tags to add to parameters synced by Infisical.",
|
||||
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as resource tags to parameters synced by Infisical.`
|
||||
},
|
||||
AWS_SECRETS_MANAGER: {
|
||||
keyId: "The AWS KMS key ID or alias to use when encrypting parameters synced by Infisical.",
|
||||
tags: "Optional tags to add to secrets synced by Infisical.",
|
||||
syncSecretMetadataAsTags: `Whether Infisical secret metadata should be added as tags to secrets synced by Infisical.`
|
||||
}
|
||||
},
|
||||
DESTINATION_CONFIG: {
|
||||
AWS_PARAMETER_STORE: {
|
||||
region: "The AWS region to sync secrets to.",
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
@ -103,6 +103,16 @@ export const isValidIpOrCidr = (ip: string): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isValidIp = (ip: string) => {
|
||||
return net.isIPv4(ip) || net.isIPv6(ip);
|
||||
};
|
||||
|
||||
export const isValidHostname = (name: string) => {
|
||||
const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
|
||||
|
||||
return hostnameRegex.test(name);
|
||||
};
|
||||
|
||||
export type TIp = {
|
||||
ipAddress: string;
|
||||
type: IPType;
|
||||
|
@ -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";
|
||||
@ -35,6 +39,12 @@ import { HsmModule } from "@app/ee/services/hsm/hsm-types";
|
||||
import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal";
|
||||
import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||
import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
|
||||
import { kmipClientCertificateDALFactory } from "@app/ee/services/kmip/kmip-client-certificate-dal";
|
||||
import { kmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
|
||||
import { kmipOperationServiceFactory } from "@app/ee/services/kmip/kmip-operation-service";
|
||||
import { kmipOrgConfigDALFactory } from "@app/ee/services/kmip/kmip-org-config-dal";
|
||||
import { kmipOrgServerCertificateDALFactory } from "@app/ee/services/kmip/kmip-org-server-certificate-dal";
|
||||
import { kmipServiceFactory } from "@app/ee/services/kmip/kmip-service";
|
||||
import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal";
|
||||
import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
|
||||
import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal";
|
||||
@ -382,6 +392,14 @@ export const registerRoutes = async (
|
||||
|
||||
const projectTemplateDAL = projectTemplateDALFactory(db);
|
||||
const resourceMetadataDAL = resourceMetadataDALFactory(db);
|
||||
const kmipClientDAL = kmipClientDALFactory(db);
|
||||
const kmipClientCertificateDAL = kmipClientCertificateDALFactory(db);
|
||||
const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db);
|
||||
const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db);
|
||||
|
||||
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
|
||||
const gatewayDAL = gatewayDALFactory(db);
|
||||
const projectGatewayDAL = projectGatewayDALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
@ -1078,7 +1096,9 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
secretSharingDAL,
|
||||
orgDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
smtpService,
|
||||
userDAL
|
||||
});
|
||||
|
||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||
@ -1290,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,
|
||||
@ -1308,8 +1340,10 @@ export const registerRoutes = async (
|
||||
folderDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectGatewayDAL
|
||||
});
|
||||
|
||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||
projectDAL,
|
||||
permissionService,
|
||||
@ -1429,6 +1463,24 @@ export const registerRoutes = async (
|
||||
keyStore
|
||||
});
|
||||
|
||||
const kmipService = kmipServiceFactory({
|
||||
kmipClientDAL,
|
||||
permissionService,
|
||||
kmipClientCertificateDAL,
|
||||
kmipOrgConfigDAL,
|
||||
kmsService,
|
||||
kmipOrgServerCertificateDAL,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const kmipOperationService = kmipOperationServiceFactory({
|
||||
kmsService,
|
||||
kmsDAL,
|
||||
projectDAL,
|
||||
kmipClientDAL,
|
||||
permissionService
|
||||
});
|
||||
|
||||
await superAdminService.initServerCfg();
|
||||
|
||||
// setup the communication with license key server
|
||||
@ -1527,7 +1579,10 @@ export const registerRoutes = async (
|
||||
projectTemplate: projectTemplateService,
|
||||
totp: totpService,
|
||||
appConnection: appConnectionService,
|
||||
secretSync: secretSyncService
|
||||
secretSync: secretSyncService,
|
||||
kmip: kmipService,
|
||||
kmipOperation: kmipOperationService,
|
||||
gateway: gatewayService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
@ -1539,7 +1594,8 @@ export const registerRoutes = async (
|
||||
}
|
||||
|
||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||
user: userDAL
|
||||
user: userDAL,
|
||||
kmipClient: kmipClientDAL
|
||||
});
|
||||
|
||||
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
|
||||
|
@ -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({
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { z } from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateAwsConnectionSchema,
|
||||
SanitizedAwsConnectionSchema,
|
||||
UpdateAwsConnectionSchema
|
||||
} from "@app/services/app-connection/aws";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
||||
export const registerAwsConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.AWS,
|
||||
server,
|
||||
@ -15,3 +21,42 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
||||
createSchema: CreateAwsConnectionSchema,
|
||||
updateSchema: UpdateAwsConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/kms-keys`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
region: z.nativeEnum(AWSRegion),
|
||||
destination: z.enum([SecretSync.AWSParameterStore, SecretSync.AWSSecretsManager])
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
kmsKeys: z.object({ alias: z.string(), id: z.string() }).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const kmsKeys = await server.services.appConnection.aws.listKmsKeys(
|
||||
{
|
||||
connectionId,
|
||||
...req.query
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
return { kmsKeys };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -2,10 +2,9 @@ import jwt from "jsonwebtoken";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -21,18 +20,19 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
|
||||
handler: async (req, res) => {
|
||||
const { decodedToken } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
||||
const appCfg = getConfig();
|
||||
if (req.auth.authMode === AuthMode.JWT) {
|
||||
await server.services.login.logout(req.permission.id, req.auth.tokenVersionId);
|
||||
}
|
||||
|
||||
await server.services.login.logout(decodedToken.userId, decodedToken.tokenVersionId);
|
||||
|
||||
void res.cookie("jid", "", {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: appCfg.HTTPS_ENABLED
|
||||
});
|
||||
|
||||
return { message: "Successfully logged out" };
|
||||
}
|
||||
});
|
||||
@ -69,37 +69,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const refreshToken = req.cookies.jid;
|
||||
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
||||
const appCfg = getConfig();
|
||||
if (!refreshToken)
|
||||
throw new NotFoundError({
|
||||
name: "AuthTokenNotFound",
|
||||
message: "Failed to find refresh token"
|
||||
});
|
||||
|
||||
const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload;
|
||||
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN)
|
||||
throw new UnauthorizedError({
|
||||
message: "The token provided is not a refresh token",
|
||||
name: "InvalidToken"
|
||||
});
|
||||
|
||||
const tokenVersion = await server.services.authToken.getUserTokenSessionById(
|
||||
decodedToken.tokenVersionId,
|
||||
decodedToken.userId
|
||||
);
|
||||
if (!tokenVersion)
|
||||
throw new UnauthorizedError({
|
||||
message: "Valid token version not found",
|
||||
name: "InvalidToken"
|
||||
});
|
||||
|
||||
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Token version mismatch",
|
||||
name: "InvalidToken"
|
||||
});
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
|
@ -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({
|
||||
|
@ -20,6 +20,7 @@ import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ProjectFilterType } from "@app/services/project/project-types";
|
||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types";
|
||||
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
import { secretRawSchema } from "../sanitizedSchemas";
|
||||
@ -536,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([
|
||||
@ -639,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()
|
||||
@ -2030,6 +2041,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
.default("/")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(RAW_SECRETS.UPDATE.secretPath),
|
||||
mode: z
|
||||
.nativeEnum(SecretUpdateMode)
|
||||
.optional()
|
||||
.default(SecretUpdateMode.FailOnNotFound)
|
||||
.describe(RAW_SECRETS.UPDATE.mode),
|
||||
secrets: z
|
||||
.object({
|
||||
secretKey: SecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName),
|
||||
@ -2037,11 +2053,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
.string()
|
||||
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
||||
.describe(RAW_SECRETS.UPDATE.secretValue),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform(removeTrailingSlash)
|
||||
.optional()
|
||||
.describe(RAW_SECRETS.UPDATE.secretPath),
|
||||
secretComment: z.string().trim().optional().describe(RAW_SECRETS.UPDATE.secretComment),
|
||||
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()
|
||||
@ -2073,7 +2100,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
environment,
|
||||
projectSlug,
|
||||
projectId: req.body.workspaceId,
|
||||
secrets: inputSecrets
|
||||
secrets: inputSecrets,
|
||||
mode: req.body.mode
|
||||
});
|
||||
if (secretOperation.type === SecretProtectionType.Approval) {
|
||||
return { approval: secretOperation.approval };
|
||||
@ -2092,15 +2120,39 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
environment: req.body.environment,
|
||||
secretPath: req.body.secretPath,
|
||||
secrets: secrets.map((secret) => ({
|
||||
secretId: secret.id,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
}))
|
||||
secrets: secrets
|
||||
.filter((el) => el.version > 1)
|
||||
.map((secret) => ({
|
||||
secretId: secret.id,
|
||||
secretPath: secret.secretPath,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
const createdSecrets = secrets.filter((el) => el.version === 1);
|
||||
if (createdSecrets.length) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: secrets[0].workspace,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.CREATE_SECRETS,
|
||||
metadata: {
|
||||
environment: req.body.environment,
|
||||
secretPath: req.body.secretPath,
|
||||
secrets: createdSecrets.map((secret) => ({
|
||||
secretId: secret.id,
|
||||
secretPath: secret.secretPath,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretUpdated,
|
||||
|
@ -22,18 +22,19 @@ import {
|
||||
TUpdateAppConnectionDTO,
|
||||
TValidateAppConnectionCredentials
|
||||
} from "@app/services/app-connection/app-connection-types";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "@app/services/app-connection/databricks";
|
||||
import { databricksConnectionService } from "@app/services/app-connection/databricks/databricks-connection-service";
|
||||
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
|
||||
import { githubConnectionService } from "@app/services/app-connection/github/github-connection-service";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||
import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||
import { githubConnectionService } from "./github/github-connection-service";
|
||||
|
||||
export type TAppConnectionServiceFactoryDep = {
|
||||
appConnectionDAL: TAppConnectionDALFactory;
|
||||
@ -369,6 +370,7 @@ export const appConnectionServiceFactory = ({
|
||||
listAvailableAppConnectionsForUser,
|
||||
github: githubConnectionService(connectAppConnectionById),
|
||||
gcp: gcpConnectionService(connectAppConnectionById),
|
||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
aws: awsConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
TAwsConnection,
|
||||
TAwsConnectionConfig,
|
||||
@ -16,6 +17,7 @@ import {
|
||||
TGitHubConnectionInput,
|
||||
TValidateGitHubConnectionCredentials
|
||||
} from "@app/services/app-connection/github";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import {
|
||||
TAzureAppConfigurationConnection,
|
||||
@ -73,3 +75,9 @@ export type TValidateAppConnectionCredentials =
|
||||
| TValidateAzureKeyVaultConnectionCredentials
|
||||
| TValidateAzureAppConfigurationConnectionCredentials
|
||||
| TValidateDatabricksConnectionCredentials;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
region: AWSRegion;
|
||||
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||
};
|
||||
|
@ -0,0 +1,88 @@
|
||||
import AWS from "aws-sdk";
|
||||
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TListAwsConnectionKmsKeys } from "@app/services/app-connection/app-connection-types";
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TAwsConnection>;
|
||||
|
||||
const listAwsKmsKeys = async (
|
||||
appConnection: TAwsConnection,
|
||||
{ region, destination }: Pick<TListAwsConnectionKmsKeys, "region" | "destination">
|
||||
) => {
|
||||
const { credentials } = await getAwsConnectionConfig(appConnection, region);
|
||||
|
||||
const awsKms = new AWS.KMS({
|
||||
credentials,
|
||||
region
|
||||
});
|
||||
|
||||
const aliasEntries: AWS.KMS.AliasList = [];
|
||||
let aliasMarker: string | undefined;
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await awsKms.listAliases({ Limit: 100, Marker: aliasMarker }).promise();
|
||||
aliasEntries.push(...(response.Aliases || []));
|
||||
aliasMarker = response.NextMarker;
|
||||
} while (aliasMarker);
|
||||
|
||||
const keyMetadataRecord: Record<string, AWS.KMS.KeyMetadata | undefined> = {};
|
||||
for await (const aliasEntry of aliasEntries) {
|
||||
if (aliasEntry.TargetKeyId) {
|
||||
const keyDescription = await awsKms.describeKey({ KeyId: aliasEntry.TargetKeyId }).promise();
|
||||
|
||||
keyMetadataRecord[aliasEntry.TargetKeyId] = keyDescription.KeyMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
const validAliasEntries = aliasEntries.filter((aliasEntry) => {
|
||||
if (!aliasEntry.TargetKeyId) return false;
|
||||
|
||||
if (destination === SecretSync.AWSParameterStore && aliasEntry.AliasName === "alias/aws/ssm") return true;
|
||||
|
||||
if (destination === SecretSync.AWSSecretsManager && aliasEntry.AliasName === "alias/aws/secretsmanager")
|
||||
return true;
|
||||
|
||||
if (aliasEntry.AliasName?.includes("alias/aws/")) return false;
|
||||
|
||||
const keyMetadata = keyMetadataRecord[aliasEntry.TargetKeyId];
|
||||
|
||||
if (!keyMetadata || keyMetadata.KeyUsage !== "ENCRYPT_DECRYPT" || keyMetadata.KeySpec !== "SYMMETRIC_DEFAULT")
|
||||
return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const kmsKeys = validAliasEntries.map((aliasEntry) => {
|
||||
return {
|
||||
id: aliasEntry.TargetKeyId!,
|
||||
alias: aliasEntry.AliasName!
|
||||
};
|
||||
});
|
||||
|
||||
return kmsKeys;
|
||||
};
|
||||
|
||||
export const awsConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listKmsKeys = async (
|
||||
{ connectionId, region, destination }: TListAwsConnectionKmsKeys,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const appConnection = await getAppConnection(AppConnection.AWS, connectionId, actor);
|
||||
|
||||
const kmsKeys = await listAwsKmsKeys(appConnection, { region, destination });
|
||||
|
||||
return kmsKeys;
|
||||
};
|
||||
|
||||
return {
|
||||
listKmsKeys
|
||||
};
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
||||
@ -8,7 +9,7 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||
|
||||
import { AuthModeJwtTokenPayload } from "../auth/auth-type";
|
||||
import { AuthModeJwtTokenPayload, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "../auth/auth-type";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TTokenDALFactory } from "./auth-token-dal";
|
||||
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
|
||||
@ -150,6 +151,40 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
||||
|
||||
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
|
||||
|
||||
const validateRefreshToken = async (refreshToken?: string) => {
|
||||
const appCfg = getConfig();
|
||||
if (!refreshToken)
|
||||
throw new NotFoundError({
|
||||
name: "AuthTokenNotFound",
|
||||
message: "Failed to find refresh token"
|
||||
});
|
||||
|
||||
const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload;
|
||||
|
||||
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN)
|
||||
throw new UnauthorizedError({
|
||||
message: "The token provided is not a refresh token",
|
||||
name: "InvalidToken"
|
||||
});
|
||||
|
||||
const tokenVersion = await getUserTokenSessionById(decodedToken.tokenVersionId, decodedToken.userId);
|
||||
|
||||
if (!tokenVersion)
|
||||
throw new UnauthorizedError({
|
||||
message: "Valid token version not found",
|
||||
name: "InvalidToken"
|
||||
});
|
||||
|
||||
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Token version mismatch",
|
||||
name: "InvalidToken"
|
||||
});
|
||||
}
|
||||
|
||||
return { decodedToken, tokenVersion };
|
||||
};
|
||||
|
||||
// to parse jwt identity in inject identity plugin
|
||||
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => {
|
||||
const session = await tokenDAL.findOneTokenSession({
|
||||
@ -188,6 +223,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
||||
clearTokenSessionById,
|
||||
getTokenSessionByUser,
|
||||
revokeAllMySessions,
|
||||
validateRefreshToken,
|
||||
fnValidateJwtIdentity,
|
||||
getUserTokenSessionById
|
||||
};
|
||||
|
@ -35,6 +35,7 @@ export enum AuthMode {
|
||||
|
||||
export enum ActorType { // would extend to AWS, Azure, ...
|
||||
PLATFORM = "platform", // Useful for when we want to perform logging on automated actions such as integration syncs.
|
||||
KMIP_CLIENT = "kmipClient",
|
||||
USER = "user", // userIdentity
|
||||
SERVICE = "service",
|
||||
IDENTITY = "identity",
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { isValidIp } from "@app/lib/ip";
|
||||
|
||||
const isValidDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return !Number.isNaN(date.getTime());
|
||||
@ -25,7 +27,7 @@ export const validateAltNamesField = z
|
||||
if (data === "") return true;
|
||||
// Split and validate each alt name
|
||||
return data.split(", ").every((name) => {
|
||||
return hostnameRegex.test(name) || z.string().email().safeParse(name).success;
|
||||
return hostnameRegex.test(name) || z.string().email().safeParse(name).success || isValidIp(name);
|
||||
});
|
||||
},
|
||||
{
|
||||
|
@ -40,3 +40,9 @@ export const isCertChainValid = async (certificates: x509.X509Certificate[]) =>
|
||||
// chain.build() implicitly verifies the chain
|
||||
return chainItems.length === certificates.length;
|
||||
};
|
||||
|
||||
export const constructPemChainFromCerts = (certificates: x509.X509Certificate[]) =>
|
||||
certificates
|
||||
.map((cert) => cert.toString("pem"))
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
@ -2257,7 +2257,9 @@ const syncSecretsFlyio = async ({
|
||||
}
|
||||
`;
|
||||
|
||||
await request.post(
|
||||
type TFlyioErrors = { message: string }[];
|
||||
|
||||
const setSecretsResp = await request.post<{ errors?: TFlyioErrors }>(
|
||||
IntegrationUrls.FLYIO_API_URL,
|
||||
{
|
||||
query: SetSecrets,
|
||||
@ -2279,6 +2281,10 @@ const syncSecretsFlyio = async ({
|
||||
}
|
||||
);
|
||||
|
||||
if (setSecretsResp.data.errors?.length) {
|
||||
throw new Error(JSON.stringify(setSecretsResp.data.errors));
|
||||
}
|
||||
|
||||
// get secrets
|
||||
interface FlyioSecret {
|
||||
name: string;
|
||||
|
@ -93,6 +93,32 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findProjectCmeks = async (projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const result = await (tx || db.replicaNode())(TableName.KmsKey)
|
||||
.where({
|
||||
[`${TableName.KmsKey}.projectId` as "projectId"]: projectId,
|
||||
[`${TableName.KmsKey}.isReserved` as "isReserved"]: false
|
||||
})
|
||||
.join(TableName.Organization, `${TableName.KmsKey}.orgId`, `${TableName.Organization}.id`)
|
||||
.join(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`)
|
||||
.select(selectAllTableCols(TableName.KmsKey))
|
||||
.select(
|
||||
db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms).as("internalKmsEncryptionAlgorithm"),
|
||||
db.ref("version").withSchema(TableName.InternalKms).as("internalKmsVersion")
|
||||
);
|
||||
|
||||
return result.map((entry) => ({
|
||||
...KmsKeysSchema.parse(entry),
|
||||
isActive: !entry.isDisabled,
|
||||
algorithm: entry.internalKmsEncryptionAlgorithm,
|
||||
version: entry.internalKmsVersion
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find project cmeks" });
|
||||
}
|
||||
};
|
||||
|
||||
const listCmeksByProjectId = async (
|
||||
{
|
||||
projectId,
|
||||
@ -167,5 +193,5 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...kmsOrm, findByIdWithAssociatedKms, listCmeksByProjectId, findCmekById, findCmekByName };
|
||||
return { ...kmsOrm, findByIdWithAssociatedKms, listCmeksByProjectId, findCmekById, findCmekByName, findProjectCmeks };
|
||||
};
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { Knex } from "knex";
|
||||
|
||||
export type TKmsRootConfigDALFactory = ReturnType<typeof kmsRootConfigDALFactory>;
|
||||
|
||||
|
@ -37,6 +37,8 @@ import {
|
||||
TEncryptWithKmsDataKeyDTO,
|
||||
TEncryptWithKmsDTO,
|
||||
TGenerateKMSDTO,
|
||||
TGetKeyMaterialDTO,
|
||||
TImportKeyMaterialDTO,
|
||||
TUpdateProjectSecretManagerKmsKeyDTO
|
||||
} from "./kms-types";
|
||||
|
||||
@ -325,6 +327,72 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getKeyMaterial = async ({ kmsId }: TGetKeyMaterialDTO) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId);
|
||||
if (!kmsDoc) {
|
||||
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||
}
|
||||
|
||||
if (kmsDoc.isReserved) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot get key material for reserved key"
|
||||
});
|
||||
}
|
||||
|
||||
if (kmsDoc.externalKms) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot get key material for external key"
|
||||
});
|
||||
}
|
||||
|
||||
const keyCipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const kmsKey = keyCipher.decrypt(kmsDoc.internalKms?.encryptedKey as Buffer, ROOT_ENCRYPTION_KEY);
|
||||
|
||||
return kmsKey;
|
||||
};
|
||||
|
||||
const importKeyMaterial = async (
|
||||
{ key, algorithm, name, isReserved, projectId, orgId }: TImportKeyMaterialDTO,
|
||||
tx?: Knex
|
||||
) => {
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
|
||||
const expectedByteLength = getByteLengthForAlgorithm(algorithm);
|
||||
if (key.byteLength !== expectedByteLength) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes`
|
||||
});
|
||||
}
|
||||
|
||||
const encryptedKeyMaterial = cipher.encrypt(key, ROOT_ENCRYPTION_KEY);
|
||||
const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||
const dbQuery = async (db: Knex) => {
|
||||
const kmsDoc = await kmsDAL.create(
|
||||
{
|
||||
name: sanitizedName,
|
||||
orgId,
|
||||
isReserved,
|
||||
projectId
|
||||
},
|
||||
db
|
||||
);
|
||||
|
||||
await internalKmsDAL.create(
|
||||
{
|
||||
version: 1,
|
||||
encryptedKey: encryptedKeyMaterial,
|
||||
encryptionAlgorithm: algorithm,
|
||||
kmsKeyId: kmsDoc.id
|
||||
},
|
||||
db
|
||||
);
|
||||
return kmsDoc;
|
||||
};
|
||||
if (tx) return dbQuery(tx);
|
||||
const doc = await kmsDAL.transaction(async (tx2) => dbQuery(tx2));
|
||||
return doc;
|
||||
};
|
||||
|
||||
const encryptWithKmsKey = async ({ kmsId }: Omit<TEncryptWithKmsDTO, "plainText">, tx?: Knex) => {
|
||||
const kmsDoc = await kmsDAL.findByIdWithAssociatedKms(kmsId, tx);
|
||||
if (!kmsDoc) {
|
||||
@ -944,6 +1012,8 @@ export const kmsServiceFactory = ({
|
||||
getProjectKeyBackup,
|
||||
loadProjectKeyBackup,
|
||||
getKmsById,
|
||||
createCipherPairWithDataKey
|
||||
createCipherPairWithDataKey,
|
||||
getKeyMaterial,
|
||||
importKeyMaterial
|
||||
};
|
||||
};
|
||||
|
@ -61,3 +61,15 @@ export enum RootKeyEncryptionStrategy {
|
||||
Software = "SOFTWARE",
|
||||
HSM = "HSM"
|
||||
}
|
||||
export type TGetKeyMaterialDTO = {
|
||||
kmsId: string;
|
||||
};
|
||||
|
||||
export type TImportKeyMaterialDTO = {
|
||||
key: Buffer;
|
||||
algorithm: SymmetricEncryption;
|
||||
name?: string;
|
||||
isReserved: boolean;
|
||||
projectId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
@ -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,8 +42,29 @@ export const secretSharingServiceFactory = ({
|
||||
permissionService,
|
||||
secretSharingDAL,
|
||||
orgDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
smtpService,
|
||||
userDAL
|
||||
}: TSecretSharingServiceFactoryDep) => {
|
||||
const $validateSharedSecretExpiry = (expiresAt: string) => {
|
||||
if (new Date(expiresAt) < new Date()) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||
}
|
||||
|
||||
// Limit Expiry Time to 1 month
|
||||
const expiryTime = new Date(expiresAt).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||
if (expiryTime - currentTime > thirtyDays) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
||||
}
|
||||
|
||||
const fiveMins = 5 * 60 * 1000;
|
||||
if (expiryTime - currentTime < fiveMins) {
|
||||
throw new BadRequestError({ message: "Expiration time cannot be less than 5 mins" });
|
||||
}
|
||||
};
|
||||
|
||||
const createSharedSecret = async ({
|
||||
actor,
|
||||
actorId,
|
||||
@ -49,25 +80,13 @@ export const secretSharingServiceFactory = ({
|
||||
}: TCreateSharedSecretDTO) => {
|
||||
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 (new Date(expiresAt) < new Date()) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||
}
|
||||
|
||||
// Limit Expiry Time to 1 month
|
||||
const expiryTime = new Date(expiresAt).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||
if (expiryTime - currentTime > thirtyDays) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be more than 30 days" });
|
||||
}
|
||||
$validateSharedSecretExpiry(expiresAt);
|
||||
|
||||
if (secretValue.length > 10_000) {
|
||||
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||
}
|
||||
|
||||
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||
|
||||
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||
|
||||
const id = crypto.randomBytes(32).toString("hex");
|
||||
@ -80,6 +99,7 @@ export const secretSharingServiceFactory = ({
|
||||
encryptedValue: null,
|
||||
encryptedSecret,
|
||||
name,
|
||||
type: SecretSharingType.Share,
|
||||
password: hashedPassword,
|
||||
expiresAt: new Date(expiresAt),
|
||||
expiresAfterViews,
|
||||
@ -93,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,
|
||||
@ -100,17 +305,7 @@ export const secretSharingServiceFactory = ({
|
||||
expiresAfterViews,
|
||||
accessType
|
||||
}: TCreatePublicSharedSecretDTO) => {
|
||||
if (new Date(expiresAt) < new Date()) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||
}
|
||||
|
||||
// Limit Expiry Time to 1 month
|
||||
const expiryTime = new Date(expiresAt).getTime();
|
||||
const currentTime = new Date().getTime();
|
||||
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
||||
if (expiryTime - currentTime > thirtyDays) {
|
||||
throw new BadRequestError({ message: "Expiration date cannot exceed more than 30 days" });
|
||||
}
|
||||
$validateSharedSecretExpiry(expiresAt);
|
||||
|
||||
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||
@ -123,6 +318,7 @@ export const secretSharingServiceFactory = ({
|
||||
encryptedValue: null,
|
||||
iv: null,
|
||||
tag: null,
|
||||
type: SecretSharingType.Share,
|
||||
encryptedSecret,
|
||||
password: hashedPassword,
|
||||
expiresAt: new Date(expiresAt),
|
||||
@ -139,7 +335,8 @@ export const secretSharingServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
offset,
|
||||
limit
|
||||
limit,
|
||||
type
|
||||
}: TGetSharedSecretsDTO) => {
|
||||
if (!actorOrgId) throw new ForbiddenRequestError();
|
||||
|
||||
@ -155,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 {
|
||||
@ -189,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")
|
||||
});
|
||||
|
||||
@ -256,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
|
||||
@ -272,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);
|
||||
|
||||
@ -288,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;
|
||||
|
@ -7,6 +7,8 @@ import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types";
|
||||
|
||||
type TAWSParameterStoreRecord = Record<string, AWS.SSM.Parameter>;
|
||||
type TAWSParameterStoreMetadataRecord = Record<string, AWS.SSM.ParameterMetadata>;
|
||||
type TAWSParameterStoreTagsRecord = Record<string, Record<string, string>>;
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const BATCH_SIZE = 10;
|
||||
@ -80,6 +82,129 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
|
||||
return awsParameterStoreSecretsRecord;
|
||||
};
|
||||
|
||||
const getParameterMetadataByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSParameterStoreMetadataRecord> => {
|
||||
const awsParameterStoreMetadataRecord: TAWSParameterStoreMetadataRecord = {};
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
let attempt = 0;
|
||||
|
||||
while (hasNext) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const parameters = await ssm
|
||||
.describeParameters({
|
||||
MaxResults: 10,
|
||||
NextToken: nextToken,
|
||||
ParameterFilters: [
|
||||
{
|
||||
Key: "Path",
|
||||
Option: "OneLevel",
|
||||
Values: [path]
|
||||
}
|
||||
]
|
||||
})
|
||||
.promise();
|
||||
|
||||
attempt = 0;
|
||||
|
||||
if (parameters.Parameters) {
|
||||
parameters.Parameters.forEach((parameter) => {
|
||||
if (parameter.Name) {
|
||||
// no leading slash if path is '/'
|
||||
const secKey = path.length > 1 ? parameter.Name.substring(path.length) : parameter.Name;
|
||||
awsParameterStoreMetadataRecord[secKey] = parameter;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hasNext = Boolean(parameters.NextToken);
|
||||
nextToken = parameters.NextToken;
|
||||
} catch (e) {
|
||||
if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
attempt += 1;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await sleep();
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return awsParameterStoreMetadataRecord;
|
||||
};
|
||||
|
||||
const getParameterStoreTagsRecord = async (
|
||||
ssm: AWS.SSM,
|
||||
awsParameterStoreSecretsRecord: TAWSParameterStoreRecord,
|
||||
needsTagsPermissions: boolean
|
||||
): Promise<{ shouldManageTags: boolean; awsParameterStoreTagsRecord: TAWSParameterStoreTagsRecord }> => {
|
||||
const awsParameterStoreTagsRecord: TAWSParameterStoreTagsRecord = {};
|
||||
|
||||
for await (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
|
||||
const [key, parameter] = entry;
|
||||
|
||||
if (!parameter.Name) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const tags = await ssm
|
||||
.listTagsForResource({
|
||||
ResourceType: "Parameter",
|
||||
ResourceId: parameter.Name
|
||||
})
|
||||
.promise();
|
||||
|
||||
awsParameterStoreTagsRecord[key] = Object.fromEntries(tags.TagList?.map((tag) => [tag.Key, tag.Value]) ?? []);
|
||||
} catch (e) {
|
||||
// users aren't required to provide tag permissions to use sync so we handle gracefully if unauthorized
|
||||
// and they aren't trying to configure tags
|
||||
if ((e as AWSError).code === "AccessDeniedException") {
|
||||
if (!needsTagsPermissions) {
|
||||
return { shouldManageTags: false, awsParameterStoreTagsRecord: {} };
|
||||
}
|
||||
|
||||
throw new SecretSyncError({
|
||||
message:
|
||||
"IAM role has inadequate permissions to manage resource tags. Ensure the following polices are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource",
|
||||
shouldRetry: false
|
||||
});
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return { shouldManageTags: true, awsParameterStoreTagsRecord };
|
||||
};
|
||||
|
||||
const processParameterTags = ({
|
||||
syncTagsRecord,
|
||||
awsTagsRecord
|
||||
}: {
|
||||
syncTagsRecord: Record<string, string>;
|
||||
awsTagsRecord: Record<string, string>;
|
||||
}) => {
|
||||
const tagsToAdd: AWS.SSM.TagList = [];
|
||||
const tagKeysToRemove: string[] = [];
|
||||
|
||||
for (const syncEntry of Object.entries(syncTagsRecord)) {
|
||||
const [syncKey, syncValue] = syncEntry;
|
||||
|
||||
if (!(syncKey in awsTagsRecord) || syncValue !== awsTagsRecord[syncKey])
|
||||
tagsToAdd.push({ Key: syncKey, Value: syncValue });
|
||||
}
|
||||
|
||||
for (const awsKey of Object.keys(awsTagsRecord)) {
|
||||
if (!(awsKey in syncTagsRecord)) tagKeysToRemove.push(awsKey);
|
||||
}
|
||||
|
||||
return { tagsToAdd, tagKeysToRemove };
|
||||
};
|
||||
|
||||
const putParameter = async (
|
||||
ssm: AWS.SSM,
|
||||
params: AWS.SSM.PutParameterRequest,
|
||||
@ -98,6 +223,42 @@ const putParameter = async (
|
||||
}
|
||||
};
|
||||
|
||||
const addTagsToParameter = async (
|
||||
ssm: AWS.SSM,
|
||||
params: Omit<AWS.SSM.AddTagsToResourceRequest, "ResourceType">,
|
||||
attempt = 0
|
||||
): Promise<AWS.SSM.AddTagsToResourceResult> => {
|
||||
try {
|
||||
return await ssm.addTagsToResource({ ...params, ResourceType: "Parameter" }).promise();
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return addTagsToParameter(ssm, params, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const removeTagsFromParameter = async (
|
||||
ssm: AWS.SSM,
|
||||
params: Omit<AWS.SSM.RemoveTagsFromResourceRequest, "ResourceType">,
|
||||
attempt = 0
|
||||
): Promise<AWS.SSM.RemoveTagsFromResourceResult> => {
|
||||
try {
|
||||
return await ssm.removeTagsFromResource({ ...params, ResourceType: "Parameter" }).promise();
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return removeTagsFromParameter(ssm, params, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteParametersBatch = async (
|
||||
ssm: AWS.SSM,
|
||||
parameters: AWS.SSM.Parameter[],
|
||||
@ -132,35 +293,92 @@ const deleteParametersBatch = async (
|
||||
|
||||
export const AwsParameterStoreSyncFns = {
|
||||
syncSecrets: async (secretSync: TAwsParameterStoreSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
const { destinationConfig, syncOptions } = secretSync;
|
||||
|
||||
const ssm = await getSSM(secretSync);
|
||||
|
||||
// TODO(scott): KMS Key ID, Tags
|
||||
|
||||
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value }] = entry;
|
||||
const awsParameterStoreMetadataRecord = await getParameterMetadataByPath(ssm, destinationConfig.path);
|
||||
|
||||
// skip empty values (not allowed by AWS) or secrets that haven't changed
|
||||
if (!value || (key in awsParameterStoreSecretsRecord && awsParameterStoreSecretsRecord[key].Value === value)) {
|
||||
const { shouldManageTags, awsParameterStoreTagsRecord } = await getParameterStoreTagsRecord(
|
||||
ssm,
|
||||
awsParameterStoreSecretsRecord,
|
||||
Boolean(syncOptions.tags?.length || syncOptions.syncSecretMetadataAsTags)
|
||||
);
|
||||
const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []);
|
||||
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value, secretMetadata }] = entry;
|
||||
|
||||
// skip empty values (not allowed by AWS)
|
||||
if (!value) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await putParameter(ssm, {
|
||||
Name: `${destinationConfig.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: value,
|
||||
Overwrite: true
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
const keyId = syncOptions.keyId ?? "alias/aws/ssm";
|
||||
|
||||
// create parameter or update if changed
|
||||
if (
|
||||
!(key in awsParameterStoreSecretsRecord) ||
|
||||
value !== awsParameterStoreSecretsRecord[key].Value ||
|
||||
keyId !== awsParameterStoreMetadataRecord[key]?.KeyId
|
||||
) {
|
||||
try {
|
||||
await putParameter(ssm, {
|
||||
Name: `${destinationConfig.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: value,
|
||||
Overwrite: true,
|
||||
KeyId: keyId
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldManageTags) {
|
||||
const { tagsToAdd, tagKeysToRemove } = processParameterTags({
|
||||
syncTagsRecord: {
|
||||
// configured sync tags take preference over secret metadata
|
||||
...(syncOptions.syncSecretMetadataAsTags &&
|
||||
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
|
||||
...syncTagsRecord
|
||||
},
|
||||
awsTagsRecord: awsParameterStoreTagsRecord[key] ?? {}
|
||||
});
|
||||
|
||||
if (tagsToAdd.length) {
|
||||
try {
|
||||
await addTagsToParameter(ssm, {
|
||||
ResourceId: `${destinationConfig.path}${key}`,
|
||||
Tags: tagsToAdd
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tagKeysToRemove.length) {
|
||||
try {
|
||||
await removeTagsFromParameter(ssm, {
|
||||
ResourceId: `${destinationConfig.path}${key}`,
|
||||
TagKeys: tagKeysToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
|
||||
@ -20,19 +21,68 @@ const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
|
||||
});
|
||||
|
||||
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(SecretSync.AWSParameterStore).extend({
|
||||
const AwsParameterStoreSyncOptionsSchema = z.object({
|
||||
keyId: z
|
||||
.string()
|
||||
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
|
||||
.min(1, "Invalid KMS Key ID")
|
||||
.max(256, "Invalid KMS Key ID")
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.keyId),
|
||||
tags: z
|
||||
.object({
|
||||
key: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.min(1, "Resource tag key required")
|
||||
.max(128, "Resource tag key cannot exceed 128 characters"),
|
||||
value: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.max(256, "Resource tag value cannot exceed 256 characters")
|
||||
})
|
||||
.array()
|
||||
.max(50)
|
||||
.refine((items) => new Set(items.map((item) => item.key)).size === items.length, {
|
||||
message: "Resource tag keys must be unique"
|
||||
})
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.tags),
|
||||
syncSecretMetadataAsTags: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.syncSecretMetadataAsTags)
|
||||
});
|
||||
|
||||
const AwsParameterStoreSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(
|
||||
SecretSync.AWSParameterStore,
|
||||
AwsParameterStoreSyncOptionsConfig,
|
||||
AwsParameterStoreSyncOptionsSchema
|
||||
).extend({
|
||||
destination: z.literal(SecretSync.AWSParameterStore),
|
||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSParameterStore
|
||||
SecretSync.AWSParameterStore,
|
||||
AwsParameterStoreSyncOptionsConfig,
|
||||
AwsParameterStoreSyncOptionsSchema
|
||||
).extend({
|
||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSParameterStore
|
||||
SecretSync.AWSParameterStore,
|
||||
AwsParameterStoreSyncOptionsConfig,
|
||||
AwsParameterStoreSyncOptionsSchema
|
||||
).extend({
|
||||
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
@ -1,16 +1,28 @@
|
||||
import { UntagResourceCommandOutput } from "@aws-sdk/client-kms";
|
||||
import {
|
||||
BatchGetSecretValueCommand,
|
||||
CreateSecretCommand,
|
||||
CreateSecretCommandInput,
|
||||
DeleteSecretCommand,
|
||||
DeleteSecretResponse,
|
||||
DescribeSecretCommand,
|
||||
DescribeSecretCommandInput,
|
||||
ListSecretsCommand,
|
||||
SecretsManagerClient,
|
||||
TagResourceCommand,
|
||||
TagResourceCommandOutput,
|
||||
UntagResourceCommand,
|
||||
UpdateSecretCommand,
|
||||
UpdateSecretCommandInput
|
||||
} from "@aws-sdk/client-secrets-manager";
|
||||
import { AWSError } from "aws-sdk";
|
||||
import { CreateSecretResponse, SecretListEntry, SecretValueEntry } from "aws-sdk/clients/secretsmanager";
|
||||
import {
|
||||
CreateSecretResponse,
|
||||
DescribeSecretResponse,
|
||||
SecretListEntry,
|
||||
SecretValueEntry,
|
||||
Tag
|
||||
} from "aws-sdk/clients/secretsmanager";
|
||||
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
||||
@ -21,6 +33,7 @@ import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-syn
|
||||
|
||||
type TAwsSecretsRecord = Record<string, SecretListEntry>;
|
||||
type TAwsSecretValuesRecord = Record<string, SecretValueEntry>;
|
||||
type TAwsSecretDescriptionsRecord = Record<string, DescribeSecretResponse>;
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const BATCH_SIZE = 20;
|
||||
@ -135,6 +148,46 @@ const getSecretValuesRecord = async (
|
||||
return awsSecretValuesRecord;
|
||||
};
|
||||
|
||||
const describeSecret = async (
|
||||
client: SecretsManagerClient,
|
||||
input: DescribeSecretCommandInput,
|
||||
attempt = 0
|
||||
): Promise<DescribeSecretResponse> => {
|
||||
try {
|
||||
return await client.send(new DescribeSecretCommand(input));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return describeSecret(client, input, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getSecretDescriptionsRecord = async (
|
||||
client: SecretsManagerClient,
|
||||
awsSecretsRecord: TAwsSecretsRecord
|
||||
): Promise<TAwsSecretDescriptionsRecord> => {
|
||||
const awsSecretDescriptionsRecord: TAwsSecretValuesRecord = {};
|
||||
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
try {
|
||||
awsSecretDescriptionsRecord[secretKey] = await describeSecret(client, {
|
||||
SecretId: secretKey
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
secretKey,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return awsSecretDescriptionsRecord;
|
||||
};
|
||||
|
||||
const createSecret = async (
|
||||
client: SecretsManagerClient,
|
||||
input: CreateSecretCommandInput,
|
||||
@ -189,9 +242,71 @@ const deleteSecret = async (
|
||||
}
|
||||
};
|
||||
|
||||
const addTags = async (
|
||||
client: SecretsManagerClient,
|
||||
secretKey: string,
|
||||
tags: Tag[],
|
||||
attempt = 0
|
||||
): Promise<TagResourceCommandOutput> => {
|
||||
try {
|
||||
return await client.send(new TagResourceCommand({ SecretId: secretKey, Tags: tags }));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return addTags(client, secretKey, tags, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const removeTags = async (
|
||||
client: SecretsManagerClient,
|
||||
secretKey: string,
|
||||
tagKeys: string[],
|
||||
attempt = 0
|
||||
): Promise<UntagResourceCommandOutput> => {
|
||||
try {
|
||||
return await client.send(new UntagResourceCommand({ SecretId: secretKey, TagKeys: tagKeys }));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return removeTags(client, secretKey, tagKeys, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const processTags = ({
|
||||
syncTagsRecord,
|
||||
awsTagsRecord
|
||||
}: {
|
||||
syncTagsRecord: Record<string, string>;
|
||||
awsTagsRecord: Record<string, string>;
|
||||
}) => {
|
||||
const tagsToAdd: Tag[] = [];
|
||||
const tagKeysToRemove: string[] = [];
|
||||
|
||||
for (const syncEntry of Object.entries(syncTagsRecord)) {
|
||||
const [syncKey, syncValue] = syncEntry;
|
||||
|
||||
if (!(syncKey in awsTagsRecord) || syncValue !== awsTagsRecord[syncKey])
|
||||
tagsToAdd.push({ Key: syncKey, Value: syncValue });
|
||||
}
|
||||
|
||||
for (const awsKey of Object.keys(awsTagsRecord)) {
|
||||
if (!(awsKey in syncTagsRecord)) tagKeysToRemove.push(awsKey);
|
||||
}
|
||||
|
||||
return { tagsToAdd, tagKeysToRemove };
|
||||
};
|
||||
|
||||
export const AwsSecretsManagerSyncFns = {
|
||||
syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
const { destinationConfig, syncOptions } = secretSync;
|
||||
|
||||
const client = await getSecretsManagerClient(secretSync);
|
||||
|
||||
@ -199,9 +314,15 @@ export const AwsSecretsManagerSyncFns = {
|
||||
|
||||
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
||||
|
||||
const awsDescriptionsRecord = await getSecretDescriptionsRecord(client, awsSecretsRecord);
|
||||
|
||||
const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []);
|
||||
|
||||
const keyId = syncOptions.keyId ?? "alias/aws/secretsmanager";
|
||||
|
||||
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value }] = entry;
|
||||
const [key, { value, secretMetadata }] = entry;
|
||||
|
||||
// skip secrets that don't have a value set
|
||||
if (!value) {
|
||||
@ -211,15 +332,26 @@ export const AwsSecretsManagerSyncFns = {
|
||||
|
||||
if (awsSecretsRecord[key]) {
|
||||
// skip secrets that haven't changed
|
||||
if (awsValuesRecord[key]?.SecretString === value) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
if (awsValuesRecord[key]?.SecretString !== value || keyId !== awsDescriptionsRecord[key]?.KmsKeyId) {
|
||||
try {
|
||||
await updateSecret(client, {
|
||||
SecretId: key,
|
||||
SecretString: value,
|
||||
KmsKeyId: keyId
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
try {
|
||||
await updateSecret(client, {
|
||||
SecretId: key,
|
||||
SecretString: value
|
||||
await createSecret(client, {
|
||||
Name: key,
|
||||
SecretString: value,
|
||||
KmsKeyId: keyId
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
@ -227,12 +359,34 @@ export const AwsSecretsManagerSyncFns = {
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
const { tagsToAdd, tagKeysToRemove } = processTags({
|
||||
syncTagsRecord: {
|
||||
// configured sync tags take preference over secret metadata
|
||||
...(syncOptions.syncSecretMetadataAsTags &&
|
||||
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
|
||||
...syncTagsRecord
|
||||
},
|
||||
awsTagsRecord: Object.fromEntries(
|
||||
awsDescriptionsRecord[key]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
|
||||
)
|
||||
});
|
||||
|
||||
if (tagsToAdd.length) {
|
||||
try {
|
||||
await createSecret(client, {
|
||||
Name: key,
|
||||
SecretString: value
|
||||
await addTags(client, key, tagsToAdd);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tagKeysToRemove.length) {
|
||||
try {
|
||||
await removeTags(client, key, tagKeysToRemove);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
@ -261,30 +415,45 @@ export const AwsSecretsManagerSyncFns = {
|
||||
Object.fromEntries(Object.entries(secretMap).map(([key, secretData]) => [key, secretData.value]))
|
||||
);
|
||||
|
||||
if (awsValuesRecord[destinationConfig.secretName]) {
|
||||
if (awsSecretsRecord[destinationConfig.secretName]) {
|
||||
await updateSecret(client, {
|
||||
SecretId: destinationConfig.secretName,
|
||||
SecretString: secretValue
|
||||
SecretString: secretValue,
|
||||
KmsKeyId: keyId
|
||||
});
|
||||
} else {
|
||||
await createSecret(client, {
|
||||
Name: destinationConfig.secretName,
|
||||
SecretString: secretValue
|
||||
SecretString: secretValue,
|
||||
KmsKeyId: keyId
|
||||
});
|
||||
}
|
||||
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
if (secretKey === destinationConfig.secretName) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
const { tagsToAdd, tagKeysToRemove } = processTags({
|
||||
syncTagsRecord,
|
||||
awsTagsRecord: Object.fromEntries(
|
||||
awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
|
||||
)
|
||||
});
|
||||
|
||||
if (tagsToAdd.length) {
|
||||
try {
|
||||
await deleteSecret(client, secretKey);
|
||||
await addTags(client, destinationConfig.secretName, tagsToAdd);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey
|
||||
secretKey: destinationConfig.secretName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tagKeysToRemove.length) {
|
||||
try {
|
||||
await removeTags(client, destinationConfig.secretName, tagKeysToRemove);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: destinationConfig.secretName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||
.discriminatedUnion("mappingBehavior", [
|
||||
@ -38,22 +39,95 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||
})
|
||||
);
|
||||
|
||||
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(SecretSync.AWSSecretsManager).extend({
|
||||
const AwsSecretsManagerSyncOptionsSchema = z.object({
|
||||
keyId: z
|
||||
.string()
|
||||
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
|
||||
.min(1, "Invalid KMS Key ID")
|
||||
.max(256, "Invalid KMS Key ID")
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.keyId),
|
||||
tags: z
|
||||
.object({
|
||||
key: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.min(1, "Tag key required")
|
||||
.max(128, "Tag key cannot exceed 128 characters"),
|
||||
value: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.max(256, "Tag value cannot exceed 256 characters")
|
||||
})
|
||||
.array()
|
||||
.max(50)
|
||||
.refine((items) => new Set(items.map((item) => item.key)).size === items.length, {
|
||||
message: "Tag keys must be unique"
|
||||
})
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.tags),
|
||||
syncSecretMetadataAsTags: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.syncSecretMetadataAsTags)
|
||||
});
|
||||
|
||||
const AwsSecretsManagerSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(
|
||||
SecretSync.AWSSecretsManager,
|
||||
AwsSecretsManagerSyncOptionsConfig,
|
||||
AwsSecretsManagerSyncOptionsSchema
|
||||
).extend({
|
||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSSecretsManager
|
||||
).extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
});
|
||||
SecretSync.AWSSecretsManager,
|
||||
AwsSecretsManagerSyncOptionsConfig,
|
||||
AwsSecretsManagerSyncOptionsSchema
|
||||
)
|
||||
.extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
})
|
||||
.superRefine((sync, ctx) => {
|
||||
if (
|
||||
sync.destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne &&
|
||||
sync.syncOptions.syncSecretMetadataAsTags
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Syncing secret metadata is not supported with "Many-to-One" mapping behavior.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSSecretsManager
|
||||
).extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
SecretSync.AWSSecretsManager,
|
||||
AwsSecretsManagerSyncOptionsConfig,
|
||||
AwsSecretsManagerSyncOptionsSchema
|
||||
)
|
||||
.extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
||||
})
|
||||
.superRefine((sync, ctx) => {
|
||||
if (
|
||||
sync.destinationConfig?.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.ManyToOne &&
|
||||
sync.syncOptions.syncSecretMetadataAsTags
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Syncing secret metadata is not supported with "Many-to-One" mapping behavior.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const AwsSecretsManagerSyncListItemSchema = z.object({
|
||||
name: z.literal("AWS Secrets Manager"),
|
||||
|
@ -233,6 +233,7 @@ export const secretSyncQueueFactory = ({
|
||||
}
|
||||
|
||||
secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
||||
secretMap[secretKey].secretMetadata = secret.secretMetadata;
|
||||
})
|
||||
);
|
||||
|
||||
@ -258,7 +259,8 @@ export const secretSyncQueueFactory = ({
|
||||
secretMap[importedSecret.key] = {
|
||||
skipMultilineEncoding: importedSecret.skipMultilineEncoding,
|
||||
comment: importedSecret.secretComment,
|
||||
value: importedSecret.secretValue || ""
|
||||
value: importedSecret.secretValue || "",
|
||||
secretMetadata: importedSecret.secretMetadata
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user