mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-29 22:37:44 +00:00
Compare commits
253 Commits
project-te
...
misc/add-p
Author | SHA1 | Date | |
---|---|---|---|
|
d185dbb7ff | ||
|
4292cb2a04 | ||
|
051f53c66e | ||
|
99daa43fc6 | ||
|
27badad3d7 | ||
|
b5e3af6e7d | ||
|
280fbdfbb9 | ||
|
18fc10aaec | ||
|
b20e04bdeb | ||
|
10d14edc20 | ||
|
4abdd4216b | ||
|
332ed68c13 | ||
|
52feabd786 | ||
|
d7a99db66a | ||
|
fc0bdc25af | ||
|
5ffe45eaf5 | ||
|
8f795100ea | ||
|
8d8a3efd77 | ||
|
677180548b | ||
|
293bea474e | ||
|
bc4fc9a1ca | ||
|
483850441d | ||
|
4355fd09cc | ||
|
1f85d9c486 | ||
|
75d33820b3 | ||
|
074446df1f | ||
|
7ffa0ef8f5 | ||
|
5250e7c3d5 | ||
|
2deaa4eff3 | ||
|
0b6bc4c1f0 | ||
|
abbe7bbd0c | ||
|
565340dc50 | ||
|
36c428f152 | ||
|
f97826ea82 | ||
|
0f5cbf055c | ||
|
1345ff02e3 | ||
|
b960ee61d7 | ||
|
0b98a214a7 | ||
|
599c2226e4 | ||
|
8e24a4d3f8 | ||
|
27486e7600 | ||
|
979e9efbcb | ||
|
e06b5ecd1b | ||
|
1097ec64b2 | ||
|
93fe9929b7 | ||
|
aca654a993 | ||
|
b5cf237a4a | ||
|
6efb630200 | ||
|
151ede6cbf | ||
|
931ee1e8da | ||
|
0401793d38 | ||
|
0613c12508 | ||
|
60d3ffac5d | ||
|
5e192539a1 | ||
|
021a8ddace | ||
|
f92aba14cd | ||
|
fdeefcdfcf | ||
|
645f70f770 | ||
|
923feb81f3 | ||
|
16c51af340 | ||
|
9fd37ca456 | ||
|
92bebf7d84 | ||
|
df053bbae9 | ||
|
42319f01a7 | ||
|
0ea9f9b60d | ||
|
33ce783fda | ||
|
63c48dc095 | ||
|
16eefe5bac | ||
|
b984111a73 | ||
|
677ff62b5c | ||
|
8cc2e08f24 | ||
|
d90178f49a | ||
|
ad50cff184 | ||
|
8e43d2a994 | ||
|
7074fdbac3 | ||
|
ef70de1e0b | ||
|
7e9ee7b5e3 | ||
|
517c613d05 | ||
|
ae8cf06ec6 | ||
|
818778ddc5 | ||
|
2e12d9a13c | ||
|
e678c9d1cf | ||
|
da0b07ce2a | ||
|
3306a9ca69 | ||
|
e9af34a6ba | ||
|
3de8ed169f | ||
|
d1eb350bdd | ||
|
0c1ccf7c2e | ||
|
d268f52a1c | ||
|
c519cee5d1 | ||
|
b55a39dd24 | ||
|
7b880f85cc | ||
|
c7dc595e1a | ||
|
6e494f198b | ||
|
e1f3eaf1a0 | ||
|
be26dc9872 | ||
|
aaeb6e73fe | ||
|
1e11702c58 | ||
|
3b81cdb16e | ||
|
6584166815 | ||
|
827cb35194 | ||
|
89a6a0ba13 | ||
|
3b9a50d65d | ||
|
beb7200233 | ||
|
18e3d132a2 | ||
|
52f8c6adba | ||
|
3d2b2cbbab | ||
|
1a82809bd5 | ||
|
c4f994750d | ||
|
fa7020949c | ||
|
eca2b3ccde | ||
|
67fc16ecd3 | ||
|
f85add7cca | ||
|
3f74d3a80d | ||
|
4a44dc6119 | ||
|
dd4bc4bc73 | ||
|
6188de43e4 | ||
|
36310387e0 | ||
|
43f3960225 | ||
|
2f0a442866 | ||
|
7e05bc86a9 | ||
|
b0c4fddf86 | ||
|
f5578d39a6 | ||
|
cd028ae133 | ||
|
63c71fabcd | ||
|
e90166f1f0 | ||
|
5a3fbc0401 | ||
|
7c52e000cd | ||
|
cccd4ba9e5 | ||
|
63f0f8e299 | ||
|
c8a3837432 | ||
|
2dd407b136 | ||
|
4e1a5565d8 | ||
|
bae62421ae | ||
|
d397002704 | ||
|
f5b1f671e3 | ||
|
0597c5f0c0 | ||
|
eb3afc8034 | ||
|
b67457fe93 | ||
|
75abdbe938 | ||
|
9b6a315825 | ||
|
13b2f65b7e | ||
|
6cf1e046b0 | ||
|
f6e1441dc0 | ||
|
7ed96164e5 | ||
|
9eeb72ac80 | ||
|
f6e566a028 | ||
|
a34c74e958 | ||
|
eef7a875a1 | ||
|
09938a911b | ||
|
af08c41008 | ||
|
443c8854ea | ||
|
f7a25e7601 | ||
|
4c6e5c9c4c | ||
|
98a4e6c96d | ||
|
c93ce06409 | ||
|
672e4baec4 | ||
|
8adf4787b9 | ||
|
a12522db55 | ||
|
49ab487dc2 | ||
|
daf0731580 | ||
|
b5ef2a6837 | ||
|
9c611daada | ||
|
71edb08942 | ||
|
89d8261a43 | ||
|
a2b2b07185 | ||
|
76864ababa | ||
|
52858dad79 | ||
|
1d7a6ea50e | ||
|
c031233247 | ||
|
d17d40ebd9 | ||
|
70fff1f2da | ||
|
3f8eaa0679 | ||
|
50d0035d7b | ||
|
9743ad02d5 | ||
|
50f5248e3e | ||
|
8d7b573988 | ||
|
26d0ab1dc2 | ||
|
4acdbd24e9 | ||
|
c3c907788a | ||
|
bf833a57cd | ||
|
e8519f6612 | ||
|
0b4675e7b5 | ||
|
091e521180 | ||
|
07df6803a5 | ||
|
d5dbc7d7e0 | ||
|
a09d0e8948 | ||
|
0af9415aa6 | ||
|
fb2b64cb19 | ||
|
ee598560ec | ||
|
2793ac22aa | ||
|
31fad03af8 | ||
|
ce612877b8 | ||
|
4ad8b468d5 | ||
|
5742fc648b | ||
|
c629705c9c | ||
|
aa68a3ef58 | ||
|
be10f6e52a | ||
|
40c5ff0ad6 | ||
|
ab6a2b7dbb | ||
|
81bfc04e7c | ||
|
a757fceaed | ||
|
859b643e43 | ||
|
91f71e0ef6 | ||
|
4e9e31eeb7 | ||
|
f6bc99b964 | ||
|
aea44088db | ||
|
578a0d7d93 | ||
|
cd71db416d | ||
|
9d682ca874 | ||
|
9054db80ad | ||
|
5bb8756c67 | ||
|
8b7cb4c4eb | ||
|
a6ee6fc4ea | ||
|
e584c9ea95 | ||
|
b21c17572d | ||
|
44c7be54cf | ||
|
45c08b3f09 | ||
|
57a29577fe | ||
|
2700a96df4 | ||
|
7457ef3b66 | ||
|
806df70dd7 | ||
|
8eda358c17 | ||
|
b34aabe72b | ||
|
1921763fa8 | ||
|
dfaed3c513 | ||
|
5408859a18 | ||
|
5b7627585f | ||
|
800ea5ce78 | ||
|
a6b3be72a9 | ||
|
531607dcb7 | ||
|
182de009b2 | ||
|
f1651ce171 | ||
|
e1f563dbd4 | ||
|
107cca0b62 | ||
|
72abc08f04 | ||
|
d6b31cde44 | ||
|
2c94f9ec3c | ||
|
42ad63b58d | ||
|
f2d5112585 | ||
|
9c7b25de49 | ||
|
36954a9df9 | ||
|
581840a701 | ||
|
326742c2d5 | ||
|
c891b8f5d3 | ||
|
a32bb95703 | ||
|
0410c83cef | ||
|
cf4f2ea6b1 | ||
|
bf85df7e36 | ||
|
b9070a8fa3 | ||
|
3ea450e94a | ||
|
7d0574087c | ||
|
36916704be |
@@ -28,3 +28,15 @@ frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow
|
||||
docs/cli/commands/user.mdx:generic-api-key:51
|
||||
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:76
|
||||
docs/integrations/app-connections/hashicorp-vault.mdx:generic-api-key:188
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:567
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:569
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:570
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:572
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:574
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:575
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:576
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:577
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:578
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:579
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:581
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:582
|
||||
|
@@ -133,8 +133,8 @@ RUN apt-get update && apt-get install -y \
|
||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
|
||||
&& apt-get update && apt-get install -y infisical=0.31.1 \
|
||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
|
||||
&& apt-get update && apt-get install -y infisical=0.41.2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
||||
|
@@ -127,8 +127,8 @@ RUN apt-get update && apt-get install -y \
|
||||
&& 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 \
|
||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
|
||||
&& apt-get update && apt-get install -y infisical=0.41.2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /
|
||||
|
@@ -54,8 +54,8 @@ COPY --from=build /app .
|
||||
|
||||
# Install Infisical CLI
|
||||
RUN apt-get install -y curl bash && \
|
||||
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 git
|
||||
curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
||||
apt-get update && apt-get install -y infisical=0.41.2 git
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||
CMD node healthcheck.js
|
||||
|
@@ -55,9 +55,9 @@ RUN mkdir -p /etc/softhsm2/tokens && \
|
||||
# ? App setup
|
||||
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \
|
||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
||||
apt-get update && \
|
||||
apt-get install -y infisical=0.8.1
|
||||
apt-get install -y infisical=0.41.2
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
@@ -64,9 +64,9 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
|
||||
# ? App setup
|
||||
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \
|
||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
||||
apt-get update && \
|
||||
apt-get install -y infisical=0.8.1
|
||||
apt-get install -y infisical=0.41.2
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
1952
backend/package-lock.json
generated
1952
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -38,8 +38,8 @@
|
||||
"build:frontend": "npm run build --prefix ../frontend",
|
||||
"start": "node --enable-source-maps dist/main.mjs",
|
||||
"type:check": "tsc --noEmit",
|
||||
"lint:fix": "eslint --fix --ext js,ts ./src",
|
||||
"lint": "eslint 'src/**/*.ts'",
|
||||
"lint:fix": "node --max-old-space-size=8192 ./node_modules/.bin/eslint --fix --ext js,ts ./src",
|
||||
"lint": "node --max-old-space-size=8192 ./node_modules/.bin/eslint 'src/**/*.ts'",
|
||||
"test:unit": "vitest run -c vitest.unit.config.ts",
|
||||
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
|
||||
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
|
||||
@@ -209,6 +209,7 @@
|
||||
"mysql2": "^3.9.8",
|
||||
"nanoid": "^3.3.8",
|
||||
"nodemailer": "^6.9.9",
|
||||
"oci-sdk": "^2.108.0",
|
||||
"odbc": "^2.4.9",
|
||||
"openid-client": "^5.6.5",
|
||||
"ora": "^7.0.1",
|
||||
@@ -241,6 +242,6 @@
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.22.4"
|
||||
"zod-to-json-schema": "^3.24.5"
|
||||
}
|
||||
}
|
||||
|
4
backend/src/@types/fastify.d.ts
vendored
4
backend/src/@types/fastify.d.ts
vendored
@@ -68,6 +68,7 @@ import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/
|
||||
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
|
||||
import { TIdentityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service";
|
||||
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
|
||||
import { TIdentityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
|
||||
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
|
||||
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
|
||||
@@ -80,6 +81,7 @@ import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
||||
import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
|
||||
import { TPkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
|
||||
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||
@@ -208,6 +210,7 @@ declare module "fastify" {
|
||||
identityGcpAuth: TIdentityGcpAuthServiceFactory;
|
||||
identityAwsAuth: TIdentityAwsAuthServiceFactory;
|
||||
identityAzureAuth: TIdentityAzureAuthServiceFactory;
|
||||
identityOciAuth: TIdentityOciAuthServiceFactory;
|
||||
identityOidcAuth: TIdentityOidcAuthServiceFactory;
|
||||
identityJwtAuth: TIdentityJwtAuthServiceFactory;
|
||||
identityLdapAuth: TIdentityLdapAuthServiceFactory;
|
||||
@@ -232,6 +235,7 @@ declare module "fastify" {
|
||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||
certificateEst: TCertificateEstServiceFactory;
|
||||
pkiCollection: TPkiCollectionServiceFactory;
|
||||
pkiSubscriber: TPkiSubscriberServiceFactory;
|
||||
secretScanning: TSecretScanningServiceFactory;
|
||||
license: TLicenseServiceFactory;
|
||||
trustedIp: TTrustedIpServiceFactory;
|
||||
|
16
backend/src/@types/knex.d.ts
vendored
16
backend/src/@types/knex.d.ts
vendored
@@ -119,6 +119,9 @@ import {
|
||||
TIdentityMetadata,
|
||||
TIdentityMetadataInsert,
|
||||
TIdentityMetadataUpdate,
|
||||
TIdentityOciAuths,
|
||||
TIdentityOciAuthsInsert,
|
||||
TIdentityOciAuthsUpdate,
|
||||
TIdentityOidcAuths,
|
||||
TIdentityOidcAuthsInsert,
|
||||
TIdentityOidcAuthsUpdate,
|
||||
@@ -209,6 +212,9 @@ import {
|
||||
TPkiCollections,
|
||||
TPkiCollectionsInsert,
|
||||
TPkiCollectionsUpdate,
|
||||
TPkiSubscribers,
|
||||
TPkiSubscribersInsert,
|
||||
TPkiSubscribersUpdate,
|
||||
TProjectBots,
|
||||
TProjectBotsInsert,
|
||||
TProjectBotsUpdate,
|
||||
@@ -564,6 +570,11 @@ declare module "knex/types/tables" {
|
||||
TPkiCollectionItemsInsert,
|
||||
TPkiCollectionItemsUpdate
|
||||
>;
|
||||
[TableName.PkiSubscriber]: KnexOriginal.CompositeTableType<
|
||||
TPkiSubscribers,
|
||||
TPkiSubscribersInsert,
|
||||
TPkiSubscribersUpdate
|
||||
>;
|
||||
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
||||
TUserGroupMembership,
|
||||
TUserGroupMembershipInsert,
|
||||
@@ -730,6 +741,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityAzureAuthsInsert,
|
||||
TIdentityAzureAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityOciAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityOciAuths,
|
||||
TIdentityOciAuthsInsert,
|
||||
TIdentityOciAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityOidcAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityOidcAuths,
|
||||
TIdentityOidcAuthsInsert,
|
||||
|
@@ -0,0 +1,47 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasEmail = await knex.schema.hasColumn(TableName.Users, "email");
|
||||
const hasUsername = await knex.schema.hasColumn(TableName.Users, "username");
|
||||
if (hasEmail) {
|
||||
await knex(TableName.Users)
|
||||
.where({ isGhost: false })
|
||||
.update({
|
||||
// @ts-expect-error email assume string this is expected
|
||||
email: knex.raw("lower(email)")
|
||||
});
|
||||
}
|
||||
if (hasUsername) {
|
||||
await knex.schema.raw(`
|
||||
CREATE INDEX IF NOT EXISTS ${TableName.Users}_lower_username_idx
|
||||
ON ${TableName.Users} (LOWER(username))
|
||||
`);
|
||||
|
||||
const duplicatesSubquery = knex(TableName.Users)
|
||||
.select(knex.raw("lower(username) as lowercase_username"))
|
||||
.groupBy("lowercase_username")
|
||||
.having(knex.raw("count(*)"), ">", 1);
|
||||
|
||||
// Update usernames to lowercase where they won't create duplicates
|
||||
await knex(TableName.Users)
|
||||
.where({ isGhost: false })
|
||||
.whereRaw("username <> lower(username)") // Only update if not already lowercase
|
||||
// @ts-expect-error username assume string this is expected
|
||||
.whereNotIn(knex.raw("lower(username)"), duplicatesSubquery)
|
||||
.update({
|
||||
// @ts-expect-error username assume string this is expected
|
||||
username: knex.raw("lower(username)")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasUsername = await knex.schema.hasColumn(TableName.Users, "username");
|
||||
if (hasUsername) {
|
||||
await knex.schema.raw(`
|
||||
DROP INDEX IF EXISTS ${TableName.Users}_lower_username_idx
|
||||
`);
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.SshHostLoginUserMapping, "groupId"))) {
|
||||
await knex.schema.alterTable(TableName.SshHostLoginUserMapping, (t) => {
|
||||
t.uuid("groupId").nullable();
|
||||
t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE");
|
||||
t.unique(["sshHostLoginUserId", "groupId"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.SshHostLoginUserMapping, "groupId")) {
|
||||
await knex.schema.alterTable(TableName.SshHostLoginUserMapping, (t) => {
|
||||
t.dropUnique(["sshHostLoginUserId", "groupId"]);
|
||||
t.dropColumn("groupId");
|
||||
});
|
||||
}
|
||||
}
|
46
backend/src/db/migrations/20250508160957_pki-subscriber.ts
Normal file
46
backend/src/db/migrations/20250508160957_pki-subscriber.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
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.PkiSubscriber))) {
|
||||
await knex.schema.createTable(TableName.PkiSubscriber, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.uuid("caId").nullable();
|
||||
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("SET NULL");
|
||||
t.string("name").notNullable();
|
||||
t.string("commonName").notNullable();
|
||||
t.specificType("subjectAlternativeNames", "text[]").notNullable();
|
||||
t.string("ttl").notNullable();
|
||||
t.specificType("keyUsages", "text[]").notNullable();
|
||||
t.specificType("extendedKeyUsages", "text[]").notNullable();
|
||||
t.string("status").notNullable(); // active / disabled
|
||||
t.unique(["projectId", "name"]);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.PkiSubscriber);
|
||||
}
|
||||
|
||||
const hasSubscriberCol = await knex.schema.hasColumn(TableName.Certificate, "pkiSubscriberId");
|
||||
if (!hasSubscriberCol) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.uuid("pkiSubscriberId").nullable();
|
||||
t.foreign("pkiSubscriberId").references("id").inTable(TableName.PkiSubscriber).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasSubscriberCol = await knex.schema.hasColumn(TableName.Certificate, "pkiSubscriberId");
|
||||
if (hasSubscriberCol) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.dropColumn("pkiSubscriberId");
|
||||
});
|
||||
}
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.PkiSubscriber);
|
||||
await dropOnUpdateTrigger(knex, TableName.PkiSubscriber);
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
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.IdentityOciAuth))) {
|
||||
await knex.schema.createTable(TableName.IdentityOciAuth, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
|
||||
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
|
||||
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
|
||||
t.jsonb("accessTokenTrustedIps").notNullable();
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("identityId").notNullable().unique();
|
||||
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
t.string("type").notNullable();
|
||||
|
||||
t.string("tenancyOcid").notNullable();
|
||||
t.string("allowedUsernames").nullable();
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.IdentityOciAuth);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityOciAuth);
|
||||
await dropOnUpdateTrigger(knex, TableName.IdentityOciAuth);
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasGatewayIdColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayId");
|
||||
|
||||
if (!hasGatewayIdColumn) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
|
||||
table.uuid("gatewayId").nullable();
|
||||
table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasGatewayIdColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayId");
|
||||
|
||||
if (hasGatewayIdColumn) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
|
||||
table.dropForeign("gatewayId");
|
||||
table.dropColumn("gatewayId");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,110 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { inMemoryKeyStore } from "@app/keystore/memory";
|
||||
import { selectAllTableCols } from "@app/lib/knex";
|
||||
import { initLogger } from "@app/lib/logger";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { getMigrationEnvConfig } from "./utils/env-config";
|
||||
import { getMigrationEncryptionServices } from "./utils/services";
|
||||
|
||||
// Note(daniel): We aren't dropping tables or columns in this migrations so we can easily rollback if needed.
|
||||
// In the future we need to drop the projectGatewayId on the dynamic secrets table, and drop the project_gateways table entirely.
|
||||
|
||||
const BATCH_SIZE = 500;
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
knex.replicaNode = () => {
|
||||
return knex;
|
||||
};
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId"))) {
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
|
||||
table.uuid("gatewayId").nullable();
|
||||
table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL");
|
||||
|
||||
table.index("gatewayId");
|
||||
});
|
||||
|
||||
const existingDynamicSecretsWithProjectGatewayId = await knex(TableName.DynamicSecret)
|
||||
.select(selectAllTableCols(TableName.DynamicSecret))
|
||||
.whereNotNull(`${TableName.DynamicSecret}.projectGatewayId`)
|
||||
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.id`, `${TableName.DynamicSecret}.projectGatewayId`)
|
||||
.whereNotNull(`${TableName.ProjectGateway}.gatewayId`)
|
||||
.select(
|
||||
knex.ref("projectId").withSchema(TableName.ProjectGateway).as("projectId"),
|
||||
knex.ref("gatewayId").withSchema(TableName.ProjectGateway).as("projectGatewayGatewayId")
|
||||
);
|
||||
|
||||
initLogger();
|
||||
const envConfig = getMigrationEnvConfig();
|
||||
const keyStore = inMemoryKeyStore();
|
||||
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
|
||||
|
||||
const updatedDynamicSecrets = await Promise.all(
|
||||
existingDynamicSecretsWithProjectGatewayId.map(async (existingDynamicSecret) => {
|
||||
if (!existingDynamicSecret.projectGatewayGatewayId) {
|
||||
const result = {
|
||||
...existingDynamicSecret,
|
||||
gatewayId: null
|
||||
};
|
||||
|
||||
const { projectId, projectGatewayGatewayId, ...rest } = result;
|
||||
return rest;
|
||||
}
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: existingDynamicSecret.projectId
|
||||
});
|
||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: existingDynamicSecret.projectId
|
||||
});
|
||||
|
||||
let decryptedStoredInput = JSON.parse(
|
||||
secretManagerDecryptor({ cipherTextBlob: Buffer.from(existingDynamicSecret.encryptedInput) }).toString()
|
||||
) as object;
|
||||
|
||||
// We're not removing the existing projectGatewayId from the input so we can easily rollback without having to re-encrypt the input
|
||||
decryptedStoredInput = {
|
||||
...decryptedStoredInput,
|
||||
gatewayId: existingDynamicSecret.projectGatewayGatewayId
|
||||
};
|
||||
|
||||
const encryptedInput = secretManagerEncryptor({
|
||||
plainText: Buffer.from(JSON.stringify(decryptedStoredInput))
|
||||
}).cipherTextBlob;
|
||||
|
||||
const result = {
|
||||
...existingDynamicSecret,
|
||||
encryptedInput,
|
||||
gatewayId: existingDynamicSecret.projectGatewayGatewayId
|
||||
};
|
||||
|
||||
const { projectId, projectGatewayGatewayId, ...rest } = result;
|
||||
return rest;
|
||||
})
|
||||
);
|
||||
|
||||
for (let i = 0; i < updatedDynamicSecrets.length; i += BATCH_SIZE) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await knex(TableName.DynamicSecret)
|
||||
.insert(updatedDynamicSecrets.slice(i, i + BATCH_SIZE))
|
||||
.onConflict("id")
|
||||
.merge();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
// no re-encryption needed as we keep the old projectGatewayId in the input
|
||||
if (await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId")) {
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
|
||||
table.dropForeign("gatewayId");
|
||||
table.dropColumn("gatewayId");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const columns = await knex.table(TableName.Organization).columnInfo();
|
||||
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
if (!columns.secretsProductEnabled) {
|
||||
t.boolean("secretsProductEnabled").defaultTo(true);
|
||||
}
|
||||
if (!columns.pkiProductEnabled) {
|
||||
t.boolean("pkiProductEnabled").defaultTo(true);
|
||||
}
|
||||
if (!columns.kmsProductEnabled) {
|
||||
t.boolean("kmsProductEnabled").defaultTo(true);
|
||||
}
|
||||
if (!columns.sshProductEnabled) {
|
||||
t.boolean("sshProductEnabled").defaultTo(true);
|
||||
}
|
||||
if (!columns.scannerProductEnabled) {
|
||||
t.boolean("scannerProductEnabled").defaultTo(true);
|
||||
}
|
||||
if (!columns.shareSecretsProductEnabled) {
|
||||
t.boolean("shareSecretsProductEnabled").defaultTo(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const columns = await knex.table(TableName.Organization).columnInfo();
|
||||
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
if (columns.secretsProductEnabled) {
|
||||
t.dropColumn("secretsProductEnabled");
|
||||
}
|
||||
if (columns.pkiProductEnabled) {
|
||||
t.dropColumn("pkiProductEnabled");
|
||||
}
|
||||
if (columns.kmsProductEnabled) {
|
||||
t.dropColumn("kmsProductEnabled");
|
||||
}
|
||||
if (columns.sshProductEnabled) {
|
||||
t.dropColumn("sshProductEnabled");
|
||||
}
|
||||
if (columns.scannerProductEnabled) {
|
||||
t.dropColumn("scannerProductEnabled");
|
||||
}
|
||||
if (columns.shareSecretsProductEnabled) {
|
||||
t.dropColumn("shareSecretsProductEnabled");
|
||||
}
|
||||
});
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasSecretSharingColumn = await knex.schema.hasColumn(TableName.Project, "secretSharing");
|
||||
if (!hasSecretSharingColumn) {
|
||||
await knex.schema.table(TableName.Project, (table) => {
|
||||
table.boolean("secretSharing").notNullable().defaultTo(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasSecretSharingColumn = await knex.schema.hasColumn(TableName.Project, "secretSharing");
|
||||
if (hasSecretSharingColumn) {
|
||||
await knex.schema.table(TableName.Project, (table) => {
|
||||
table.dropColumn("secretSharing");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasLifetimeColumn = await knex.schema.hasColumn(TableName.Organization, "maxSharedSecretLifetime");
|
||||
const hasViewLimitColumn = await knex.schema.hasColumn(TableName.Organization, "maxSharedSecretViewLimit");
|
||||
|
||||
if (!hasLifetimeColumn || !hasViewLimitColumn) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
if (!hasLifetimeColumn) {
|
||||
t.integer("maxSharedSecretLifetime").nullable().defaultTo(2592000); // 30 days in seconds
|
||||
}
|
||||
if (!hasViewLimitColumn) {
|
||||
t.integer("maxSharedSecretViewLimit").nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasLifetimeColumn = await knex.schema.hasColumn(TableName.Organization, "maxSharedSecretLifetime");
|
||||
const hasViewLimitColumn = await knex.schema.hasColumn(TableName.Organization, "maxSharedSecretViewLimit");
|
||||
|
||||
if (hasLifetimeColumn || hasViewLimitColumn) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
if (hasLifetimeColumn) {
|
||||
t.dropColumn("maxSharedSecretLifetime");
|
||||
}
|
||||
if (hasViewLimitColumn) {
|
||||
t.dropColumn("maxSharedSecretViewLimit");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
const hasEncryptedSalt = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSalt");
|
||||
const hasAuthorizedEmails = await knex.schema.hasColumn(TableName.SecretSharing, "authorizedEmails");
|
||||
|
||||
if (!hasEncryptedSalt || !hasAuthorizedEmails) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
// These two columns are only needed when secrets are shared with a specific list of emails
|
||||
|
||||
if (!hasEncryptedSalt) {
|
||||
t.binary("encryptedSalt").nullable();
|
||||
}
|
||||
|
||||
if (!hasAuthorizedEmails) {
|
||||
t.json("authorizedEmails").nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
const hasEncryptedSalt = await knex.schema.hasColumn(TableName.SecretSharing, "encryptedSalt");
|
||||
const hasAuthorizedEmails = await knex.schema.hasColumn(TableName.SecretSharing, "authorizedEmails");
|
||||
|
||||
if (hasEncryptedSalt || hasAuthorizedEmails) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
if (hasEncryptedSalt) {
|
||||
t.dropColumn("encryptedSalt");
|
||||
}
|
||||
|
||||
if (hasAuthorizedEmails) {
|
||||
t.dropColumn("authorizedEmails");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -24,7 +24,8 @@ export const CertificatesSchema = z.object({
|
||||
caCertId: z.string().uuid(),
|
||||
certificateTemplateId: z.string().uuid().nullable().optional(),
|
||||
keyUsages: z.string().array().nullable().optional(),
|
||||
extendedKeyUsages: z.string().array().nullable().optional()
|
||||
extendedKeyUsages: z.string().array().nullable().optional(),
|
||||
pkiSubscriberId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||
|
@@ -27,7 +27,8 @@ export const DynamicSecretsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
encryptedInput: zodBuffer,
|
||||
projectGatewayId: z.string().uuid().nullable().optional()
|
||||
projectGatewayId: z.string().uuid().nullable().optional(),
|
||||
gatewayId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||
|
@@ -29,7 +29,8 @@ export const IdentityKubernetesAuthsSchema = z.object({
|
||||
allowedNames: z.string(),
|
||||
allowedAudience: z.string(),
|
||||
encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(),
|
||||
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional()
|
||||
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(),
|
||||
gatewayId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>;
|
||||
|
26
backend/src/db/schemas/identity-oci-auths.ts
Normal file
26
backend/src/db/schemas/identity-oci-auths.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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 IdentityOciAuthsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
accessTokenTTL: z.coerce.number().default(7200),
|
||||
accessTokenMaxTTL: z.coerce.number().default(7200),
|
||||
accessTokenNumUsesLimit: z.coerce.number().default(0),
|
||||
accessTokenTrustedIps: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
identityId: z.string().uuid(),
|
||||
type: z.string(),
|
||||
tenancyOcid: z.string(),
|
||||
allowedUsernames: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TIdentityOciAuths = z.infer<typeof IdentityOciAuthsSchema>;
|
||||
export type TIdentityOciAuthsInsert = Omit<z.input<typeof IdentityOciAuthsSchema>, TImmutableDBKeys>;
|
||||
export type TIdentityOciAuthsUpdate = Partial<Omit<z.input<typeof IdentityOciAuthsSchema>, TImmutableDBKeys>>;
|
@@ -37,6 +37,7 @@ export * from "./identity-gcp-auths";
|
||||
export * from "./identity-jwt-auths";
|
||||
export * from "./identity-kubernetes-auths";
|
||||
export * from "./identity-metadata";
|
||||
export * from "./identity-oci-auths";
|
||||
export * from "./identity-oidc-auths";
|
||||
export * from "./identity-org-memberships";
|
||||
export * from "./identity-project-additional-privilege";
|
||||
@@ -69,6 +70,7 @@ export * from "./organizations";
|
||||
export * from "./pki-alerts";
|
||||
export * from "./pki-collection-items";
|
||||
export * from "./pki-collections";
|
||||
export * from "./pki-subscribers";
|
||||
export * from "./project-bots";
|
||||
export * from "./project-environments";
|
||||
export * from "./project-gateways";
|
||||
|
@@ -21,6 +21,7 @@ export enum TableName {
|
||||
CertificateBody = "certificate_bodies",
|
||||
CertificateSecret = "certificate_secrets",
|
||||
CertificateTemplate = "certificate_templates",
|
||||
PkiSubscriber = "pki_subscribers",
|
||||
PkiAlert = "pki_alerts",
|
||||
PkiCollection = "pki_collections",
|
||||
PkiCollectionItem = "pki_collection_items",
|
||||
@@ -78,6 +79,7 @@ export enum TableName {
|
||||
IdentityAzureAuth = "identity_azure_auths",
|
||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||
IdentityAwsAuth = "identity_aws_auths",
|
||||
IdentityOciAuth = "identity_oci_auths",
|
||||
IdentityOidcAuth = "identity_oidc_auths",
|
||||
IdentityJwtAuth = "identity_jwt_auths",
|
||||
IdentityLdapAuth = "identity_ldap_auths",
|
||||
@@ -232,6 +234,7 @@ export enum IdentityAuthMethod {
|
||||
GCP_AUTH = "gcp-auth",
|
||||
AWS_AUTH = "aws-auth",
|
||||
AZURE_AUTH = "azure-auth",
|
||||
OCI_AUTH = "oci-auth",
|
||||
OIDC_AUTH = "oidc-auth",
|
||||
JWT_AUTH = "jwt-auth",
|
||||
LDAP_AUTH = "ldap-auth"
|
||||
|
@@ -28,7 +28,15 @@ export const OrganizationsSchema = z.object({
|
||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
||||
bypassOrgAuthEnabled: z.boolean().default(false),
|
||||
userTokenExpiration: z.string().nullable().optional()
|
||||
userTokenExpiration: z.string().nullable().optional(),
|
||||
secretsProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
pkiProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
kmsProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
sshProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
|
||||
maxSharedSecretViewLimit: z.number().nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
27
backend/src/db/schemas/pki-subscribers.ts
Normal file
27
backend/src/db/schemas/pki-subscribers.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// 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 PkiSubscribersSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
projectId: z.string(),
|
||||
caId: z.string().uuid().nullable().optional(),
|
||||
name: z.string(),
|
||||
commonName: z.string(),
|
||||
subjectAlternativeNames: z.string().array(),
|
||||
ttl: z.string(),
|
||||
keyUsages: z.string().array(),
|
||||
extendedKeyUsages: z.string().array(),
|
||||
status: z.string()
|
||||
});
|
||||
|
||||
export type TPkiSubscribers = z.infer<typeof PkiSubscribersSchema>;
|
||||
export type TPkiSubscribersInsert = Omit<z.input<typeof PkiSubscribersSchema>, TImmutableDBKeys>;
|
||||
export type TPkiSubscribersUpdate = Partial<Omit<z.input<typeof PkiSubscribersSchema>, TImmutableDBKeys>>;
|
@@ -27,7 +27,8 @@ export const ProjectsSchema = z.object({
|
||||
description: z.string().nullable().optional(),
|
||||
type: z.string(),
|
||||
enforceCapitalization: z.boolean().default(false),
|
||||
hasDeleteProtection: z.boolean().default(false).nullable().optional()
|
||||
hasDeleteProtection: z.boolean().default(false).nullable().optional(),
|
||||
secretSharing: z.boolean().default(true)
|
||||
});
|
||||
|
||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||
|
@@ -27,7 +27,9 @@ export const SecretSharingSchema = z.object({
|
||||
password: z.string().nullable().optional(),
|
||||
encryptedSecret: zodBuffer.nullable().optional(),
|
||||
identifier: z.string().nullable().optional(),
|
||||
type: z.string().default("share")
|
||||
type: z.string().default("share"),
|
||||
encryptedSalt: zodBuffer.nullable().optional(),
|
||||
authorizedEmails: z.unknown().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||
|
@@ -12,7 +12,8 @@ export const SshHostLoginUserMappingsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
sshHostLoginUserId: z.string().uuid(),
|
||||
userId: z.string().uuid().nullable().optional()
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
groupId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSshHostLoginUserMappings = z.infer<typeof SshHostLoginUserMappingsSchema>;
|
||||
|
@@ -121,14 +121,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
|
||||
identity: z.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
}),
|
||||
projects: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
@@ -158,17 +151,15 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
|
||||
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
|
||||
const gateways = await server.services.gateway.listGateways({
|
||||
orgPermission: req.permission
|
||||
});
|
||||
return { gateways };
|
||||
}
|
||||
@@ -216,8 +207,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
name: slugSchema({ field: "name" }).optional(),
|
||||
projectIds: z.string().array().optional()
|
||||
name: slugSchema({ field: "name" }).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -230,8 +220,7 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
|
||||
const gateway = await server.services.gateway.updateGatewayById({
|
||||
orgPermission: req.permission,
|
||||
id: req.params.id,
|
||||
name: req.body.name,
|
||||
projectIds: req.body.projectIds
|
||||
name: req.body.name
|
||||
});
|
||||
return { gateway };
|
||||
}
|
||||
|
@@ -145,7 +145,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
||||
externalId: profile.nameID,
|
||||
email,
|
||||
email: email.toLowerCase(),
|
||||
firstName,
|
||||
lastName: lastName as string,
|
||||
relayState: (req.body as { RelayState?: string }).RelayState,
|
||||
|
@@ -97,7 +97,7 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
|
||||
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
|
||||
})
|
||||
.refine((data) => ms(data.maxTTL) >= ms(data.ttl), {
|
||||
message: "Max TLL must be greater than or equal to TTL",
|
||||
message: "Max TTL must be greater than or equal to TTL",
|
||||
path: ["maxTTL"]
|
||||
}),
|
||||
response: {
|
||||
|
@@ -73,7 +73,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const host = await server.services.sshHost.getSshHost({
|
||||
const host = await server.services.sshHost.getSshHostById({
|
||||
sshHostId: req.params.sshHostId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
|
@@ -19,7 +19,7 @@ import { TProjectPermission } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
|
||||
@@ -162,6 +162,12 @@ export enum EventType {
|
||||
REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth",
|
||||
GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth",
|
||||
|
||||
LOGIN_IDENTITY_OCI_AUTH = "login-identity-oci-auth",
|
||||
ADD_IDENTITY_OCI_AUTH = "add-identity-oci-auth",
|
||||
UPDATE_IDENTITY_OCI_AUTH = "update-identity-oci-auth",
|
||||
REVOKE_IDENTITY_OCI_AUTH = "revoke-identity-oci-auth",
|
||||
GET_IDENTITY_OCI_AUTH = "get-identity-oci-auth",
|
||||
|
||||
LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth",
|
||||
ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth",
|
||||
UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth",
|
||||
@@ -254,6 +260,13 @@ export enum EventType {
|
||||
GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items",
|
||||
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
|
||||
DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item",
|
||||
CREATE_PKI_SUBSCRIBER = "create-pki-subscriber",
|
||||
UPDATE_PKI_SUBSCRIBER = "update-pki-subscriber",
|
||||
DELETE_PKI_SUBSCRIBER = "delete-pki-subscriber",
|
||||
GET_PKI_SUBSCRIBER = "get-pki-subscriber",
|
||||
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
|
||||
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
|
||||
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
|
||||
CREATE_KMS = "create-kms",
|
||||
UPDATE_KMS = "update-kms",
|
||||
DELETE_KMS = "delete-kms",
|
||||
@@ -1002,6 +1015,55 @@ interface GetIdentityAwsAuthEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityOciAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_OCI_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
identityOciAuthId: string;
|
||||
identityAccessTokenId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddIdentityOciAuthEvent {
|
||||
type: EventType.ADD_IDENTITY_OCI_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
tenancyOcid: string;
|
||||
allowedUsernames: string | null;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteIdentityOciAuthEvent {
|
||||
type: EventType.REVOKE_IDENTITY_OCI_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateIdentityOciAuthEvent {
|
||||
type: EventType.UPDATE_IDENTITY_OCI_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
tenancyOcid?: string;
|
||||
allowedUsernames: string | null;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetIdentityOciAuthEvent {
|
||||
type: EventType.GET_IDENTITY_OCI_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityAzureAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_AZURE_AUTH;
|
||||
metadata: {
|
||||
@@ -1965,6 +2027,77 @@ interface DeletePkiCollectionItem {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreatePkiSubscriber {
|
||||
type: EventType.CREATE_PKI_SUBSCRIBER;
|
||||
metadata: {
|
||||
pkiSubscriberId: string;
|
||||
caId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
ttl: string;
|
||||
subjectAlternativeNames: string[];
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdatePkiSubscriber {
|
||||
type: EventType.UPDATE_PKI_SUBSCRIBER;
|
||||
metadata: {
|
||||
pkiSubscriberId: string;
|
||||
caId?: string;
|
||||
name?: string;
|
||||
commonName?: string;
|
||||
ttl?: string;
|
||||
subjectAlternativeNames?: string[];
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DeletePkiSubscriber {
|
||||
type: EventType.DELETE_PKI_SUBSCRIBER;
|
||||
metadata: {
|
||||
pkiSubscriberId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetPkiSubscriber {
|
||||
type: EventType.GET_PKI_SUBSCRIBER;
|
||||
metadata: {
|
||||
pkiSubscriberId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface IssuePkiSubscriberCert {
|
||||
type: EventType.ISSUE_PKI_SUBSCRIBER_CERT;
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
name: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SignPkiSubscriberCert {
|
||||
type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
name: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ListPkiSubscriberCerts {
|
||||
type: EventType.LIST_PKI_SUBSCRIBER_CERTS;
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
name: string;
|
||||
projectId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateKmsEvent {
|
||||
type: EventType.CREATE_KMS;
|
||||
metadata: {
|
||||
@@ -2836,6 +2969,11 @@ export type Event =
|
||||
| UpdateIdentityAwsAuthEvent
|
||||
| GetIdentityAwsAuthEvent
|
||||
| DeleteIdentityAwsAuthEvent
|
||||
| LoginIdentityOciAuthEvent
|
||||
| AddIdentityOciAuthEvent
|
||||
| UpdateIdentityOciAuthEvent
|
||||
| GetIdentityOciAuthEvent
|
||||
| DeleteIdentityOciAuthEvent
|
||||
| LoginIdentityAzureAuthEvent
|
||||
| AddIdentityAzureAuthEvent
|
||||
| DeleteIdentityAzureAuthEvent
|
||||
@@ -2928,6 +3066,13 @@ export type Event =
|
||||
| GetPkiCollectionItems
|
||||
| AddPkiCollectionItem
|
||||
| DeletePkiCollectionItem
|
||||
| CreatePkiSubscriber
|
||||
| UpdatePkiSubscriber
|
||||
| DeletePkiSubscriber
|
||||
| GetPkiSubscriber
|
||||
| IssuePkiSubscriberCert
|
||||
| SignPkiSubscriberCert
|
||||
| ListPkiSubscriberCerts
|
||||
| CreateKmsEvent
|
||||
| UpdateKmsEvent
|
||||
| DeleteKmsEvent
|
||||
|
@@ -17,7 +17,8 @@ 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 { TGatewayDALFactory } from "../gateway/gateway-dal";
|
||||
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
||||
import {
|
||||
DynamicSecretStatus,
|
||||
@@ -44,9 +45,9 @@ type TDynamicSecretServiceFactoryDep = {
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
||||
gatewayDAL: Pick<TGatewayDALFactory, "findOne" | "find">;
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
};
|
||||
|
||||
@@ -62,7 +63,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
dynamicSecretQueueService,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
projectGatewayDAL,
|
||||
gatewayDAL,
|
||||
resourceMetadataDAL
|
||||
}: TDynamicSecretServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
@@ -117,15 +118,31 @@ export const dynamicSecretServiceFactory = ({
|
||||
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;
|
||||
if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
|
||||
const gatewayId = inputs.gatewayId as string;
|
||||
|
||||
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
|
||||
if (!projectGateway)
|
||||
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
|
||||
|
||||
if (!gateway) {
|
||||
throw new NotFoundError({
|
||||
message: `Project gateway with ${projectGatewayId} not found`
|
||||
message: `Gateway with ID ${gatewayId} not found`
|
||||
});
|
||||
selectedGatewayId = projectGateway.id;
|
||||
}
|
||||
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
gateway.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(orgPermission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.AttachGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
|
||||
selectedGatewayId = gateway.id;
|
||||
}
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
||||
@@ -146,7 +163,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
name,
|
||||
projectGatewayId: selectedGatewayId
|
||||
gatewayId: selectedGatewayId
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -255,20 +272,30 @@ export const dynamicSecretServiceFactory = ({
|
||||
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;
|
||||
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
|
||||
const gatewayId = updatedInput.gatewayId as string;
|
||||
|
||||
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
|
||||
if (!projectGateway)
|
||||
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
|
||||
if (!gateway) {
|
||||
throw new NotFoundError({
|
||||
message: `Project gateway with ${projectGatewayId} not found`
|
||||
message: `Gateway with ID ${gatewayId} not found`
|
||||
});
|
||||
selectedGatewayId = projectGateway.id;
|
||||
}
|
||||
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
gateway.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(orgPermission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.AttachGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
|
||||
selectedGatewayId = gateway.id;
|
||||
}
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||
@@ -284,7 +311,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
status: null,
|
||||
projectGatewayId: selectedGatewayId
|
||||
gatewayId: selectedGatewayId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@@ -18,7 +18,7 @@ import { SqlDatabaseProvider } from "./sql-database";
|
||||
import { TotpProvider } from "./totp";
|
||||
|
||||
type TBuildDynamicSecretProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
};
|
||||
|
||||
export const buildDynamicSecretProviders = ({
|
||||
|
@@ -137,7 +137,7 @@ export const DynamicSecretSqlDBSchema = z.object({
|
||||
revocationStatement: z.string().trim(),
|
||||
renewStatement: z.string().trim().optional(),
|
||||
ca: z.string().optional(),
|
||||
projectGatewayId: z.string().nullable().optional()
|
||||
gatewayId: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export const DynamicSecretCassandraSchema = z.object({
|
||||
|
@@ -112,14 +112,14 @@ const generateUsername = (provider: SqlProviders) => {
|
||||
};
|
||||
|
||||
type TSqlDatabaseProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
};
|
||||
|
||||
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId));
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
|
||||
validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, {
|
||||
allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val)
|
||||
});
|
||||
@@ -168,7 +168,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
|
||||
gatewayCallback: (host: string, port: number) => Promise<void>
|
||||
) => {
|
||||
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string);
|
||||
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
|
||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||
await withGatewayProxy(
|
||||
async (port) => {
|
||||
@@ -202,7 +202,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
await db.destroy();
|
||||
};
|
||||
|
||||
if (providerInputs.projectGatewayId) {
|
||||
if (providerInputs.gatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
@@ -238,7 +238,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
await db.destroy();
|
||||
}
|
||||
};
|
||||
if (providerInputs.projectGatewayId) {
|
||||
if (providerInputs.gatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
@@ -265,7 +265,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
await db.destroy();
|
||||
}
|
||||
};
|
||||
if (providerInputs.projectGatewayId) {
|
||||
if (providerInputs.gatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
@@ -301,7 +301,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
await db.destroy();
|
||||
}
|
||||
};
|
||||
if (providerInputs.projectGatewayId) {
|
||||
if (providerInputs.gatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
|
@@ -1,37 +1,34 @@
|
||||
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";
|
||||
import { buildFindFilter, ormify, selectAllTableCols, 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> = {}) => {
|
||||
const find = async (
|
||||
filter: TFindFilter<TGateways> & { orgId?: string },
|
||||
{ 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))
|
||||
.where(buildFindFilter(filter, TableName.Gateway, ["orgId"]))
|
||||
.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`)
|
||||
.join(
|
||||
TableName.IdentityOrgMembership,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.Gateway}.identityId`
|
||||
)
|
||||
.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")
|
||||
);
|
||||
.select(db.ref("orgId").withSchema(TableName.IdentityOrgMembership).as("identityOrgId"))
|
||||
.select(db.ref("name").withSchema(TableName.Identity).as("identityName"));
|
||||
|
||||
if (filter.orgId) {
|
||||
void query.where(`${TableName.IdentityOrgMembership}.orgId`, filter.orgId);
|
||||
}
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
@@ -39,48 +36,16 @@ export const gatewayDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return docs.map((el) => ({
|
||||
...GatewaysSchema.parse(el),
|
||||
orgId: el.identityOrgId as string, // todo(daniel): figure out why typescript is not inferring this as a string
|
||||
identity: { id: el.identityId, name: el.identityName }
|
||||
}));
|
||||
} 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 };
|
||||
return { ...orm, find };
|
||||
};
|
||||
|
@@ -4,7 +4,6 @@ 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";
|
||||
@@ -27,17 +26,14 @@ 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">;
|
||||
@@ -57,8 +53,7 @@ export const gatewayServiceFactory = ({
|
||||
kmsService,
|
||||
permissionService,
|
||||
orgGatewayConfigDAL,
|
||||
keyStore,
|
||||
projectGatewayDAL
|
||||
keyStore
|
||||
}: TGatewayServiceFactoryDep) => {
|
||||
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||
// if (!licenseService.onPremFeatures.gateway) {
|
||||
@@ -526,7 +521,7 @@ export const gatewayServiceFactory = ({
|
||||
return gateway;
|
||||
};
|
||||
|
||||
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => {
|
||||
const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
@@ -543,15 +538,6 @@ export const gatewayServiceFactory = ({
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -576,27 +562,7 @@ export const gatewayServiceFactory = ({
|
||||
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 fnGetGatewayClientTlsByGatewayId = async (gatewayId: string) => {
|
||||
const gateway = await gatewayDAL.findById(gatewayId);
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
|
||||
|
||||
@@ -645,8 +611,7 @@ export const gatewayServiceFactory = ({
|
||||
getGatewayById,
|
||||
updateGatewayById,
|
||||
deleteGatewayById,
|
||||
getProjectGateways,
|
||||
fnGetGatewayClientTls,
|
||||
fnGetGatewayClientTlsByGatewayId,
|
||||
heartbeat
|
||||
};
|
||||
};
|
||||
|
@@ -20,7 +20,6 @@ export type TGetGatewayByIdDTO = {
|
||||
export type TUpdateGatewayByIdDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
projectIds?: string[];
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
|
@@ -1,10 +0,0 @@
|
||||
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;
|
||||
};
|
@@ -111,9 +111,9 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
|
||||
if (search) {
|
||||
void query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", "username") ilike ?`, [`%${search}%`]);
|
||||
void query.andWhereRaw(`CONCAT_WS(' ', "firstName", "lastName", lower("username")) ilike ?`, [`%${search}%`]);
|
||||
} else if (username) {
|
||||
void query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
|
||||
void query.andWhereRaw(`lower("${TableName.Users}"."username") ilike ?`, `%${username}%`);
|
||||
}
|
||||
|
||||
switch (filter) {
|
||||
@@ -157,10 +157,23 @@ export const groupDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findGroupsByProjectId = async (projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.Groups)
|
||||
.join(TableName.GroupProjectMembership, `${TableName.Groups}.id`, `${TableName.GroupProjectMembership}.groupId`)
|
||||
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
|
||||
.select(selectAllTableCols(TableName.Groups));
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find groups by project id" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
findGroups,
|
||||
findByOrgId,
|
||||
findAllGroupPossibleMembers,
|
||||
findGroupsByProjectId,
|
||||
...groupOrm
|
||||
};
|
||||
};
|
||||
|
@@ -30,7 +30,7 @@ import {
|
||||
import { TUserGroupMembershipDALFactory } from "./user-group-membership-dal";
|
||||
|
||||
type TGroupServiceFactoryDep = {
|
||||
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findOne">;
|
||||
userDAL: Pick<TUserDALFactory, "find" | "findUserEncKeyByUserIdsBatch" | "transaction" | "findUserByUsername">;
|
||||
groupDAL: Pick<
|
||||
TGroupDALFactory,
|
||||
"create" | "findOne" | "update" | "delete" | "findAllGroupPossibleMembers" | "findById" | "transaction"
|
||||
@@ -380,7 +380,10 @@ export const groupServiceFactory = ({
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const user = await userDAL.findOne({ username });
|
||||
const usersWithUsername = await userDAL.findUserByUsername(username);
|
||||
// akhilmhdh: case sensitive email resolution
|
||||
const user =
|
||||
usersWithUsername?.length > 1 ? usersWithUsername.find((el) => el.username === username) : usersWithUsername?.[0];
|
||||
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||
|
||||
const users = await addUsersToGroupByUserIds({
|
||||
@@ -461,7 +464,10 @@ export const groupServiceFactory = ({
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const user = await userDAL.findOne({ username });
|
||||
const usersWithUsername = await userDAL.findUserByUsername(username);
|
||||
// akhilmhdh: case sensitive email resolution
|
||||
const user =
|
||||
usersWithUsername?.length > 1 ? usersWithUsername.find((el) => el.username === username) : usersWithUsername?.[0];
|
||||
if (!user) throw new NotFoundError({ message: `Failed to find user with username ${username}` });
|
||||
|
||||
const users = await removeUsersFromGroupByUserIds({
|
||||
|
@@ -176,7 +176,8 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
||||
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
||||
db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"),
|
||||
db.ref("firstName").withSchema(TableName.Users).as("firstName"),
|
||||
db.ref("lastName").withSchema(TableName.Users).as("lastName")
|
||||
db.ref("lastName").withSchema(TableName.Users).as("lastName"),
|
||||
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||
);
|
||||
|
||||
return docs;
|
||||
|
@@ -24,9 +24,13 @@ export const initializeHsmModule = (envConfig: Pick<TEnvConfig, "isHsmConfigured
|
||||
isInitialized = true;
|
||||
|
||||
logger.info("PKCS#11 module initialized");
|
||||
} catch (err) {
|
||||
logger.error(err, "Failed to initialize PKCS#11 module");
|
||||
throw err;
|
||||
} catch (error) {
|
||||
if (error instanceof pkcs11js.Pkcs11Error && error.code === pkcs11js.CKR_CRYPTOKI_ALREADY_INITIALIZED) {
|
||||
logger.info("Skipping HSM initialization because it's already initialized.");
|
||||
} else {
|
||||
logger.error(error, "Failed to initialize PKCS#11 module");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -380,7 +380,7 @@ export const ldapConfigServiceFactory = ({
|
||||
if (serverCfg.trustLdapEmails) {
|
||||
newUser = await userDAL.findOne(
|
||||
{
|
||||
email,
|
||||
email: email.toLowerCase(),
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
@@ -391,8 +391,8 @@ export const ldapConfigServiceFactory = ({
|
||||
const uniqueUsername = await normalizeUsername(username, userDAL);
|
||||
newUser = await userDAL.create(
|
||||
{
|
||||
username: serverCfg.trustLdapEmails ? email : uniqueUsername,
|
||||
email,
|
||||
username: serverCfg.trustLdapEmails ? email.toLowerCase() : uniqueUsername,
|
||||
email: email.toLowerCase(),
|
||||
isEmailVerified: serverCfg.trustLdapEmails,
|
||||
firstName,
|
||||
lastName,
|
||||
@@ -429,7 +429,7 @@ export const ldapConfigServiceFactory = ({
|
||||
await orgMembershipDAL.create(
|
||||
{
|
||||
userId: newUser.id,
|
||||
inviteEmail: email,
|
||||
inviteEmail: email.toLowerCase(),
|
||||
orgId,
|
||||
role,
|
||||
roleId,
|
||||
|
@@ -2,6 +2,7 @@ import axios, { AxiosError } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TFeatureSet } from "./license-types";
|
||||
|
||||
@@ -98,9 +99,10 @@ export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string
|
||||
(response) => response,
|
||||
async (err) => {
|
||||
const originalRequest = (err as AxiosError).config;
|
||||
|
||||
const errStatusCode = Number((err as AxiosError)?.response?.status);
|
||||
logger.error((err as AxiosError)?.response?.data, "License server call error");
|
||||
// eslint-disable-next-line
|
||||
if ((err as AxiosError)?.response?.status === 401 && !(originalRequest as any)._retry) {
|
||||
if ((errStatusCode === 401 || errStatusCode === 403) && !(originalRequest as any)._retry) {
|
||||
// eslint-disable-next-line
|
||||
(originalRequest as any)._retry = true; // injected
|
||||
|
||||
|
@@ -348,8 +348,8 @@ export const licenseServiceFactory = ({
|
||||
} = await licenseServerCloudApi.request.post(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`,
|
||||
{
|
||||
success_url: `${appCfg.SITE_URL}/dashboard`,
|
||||
cancel_url: `${appCfg.SITE_URL}/dashboard`
|
||||
success_url: `${appCfg.SITE_URL}/organization/billing`,
|
||||
cancel_url: `${appCfg.SITE_URL}/organization/billing`
|
||||
}
|
||||
);
|
||||
|
||||
@@ -362,7 +362,7 @@ export const licenseServiceFactory = ({
|
||||
} = await licenseServerCloudApi.request.post(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/billing-portal`,
|
||||
{
|
||||
return_url: `${appCfg.SITE_URL}/dashboard`
|
||||
return_url: `${appCfg.SITE_URL}/organization/billing`
|
||||
}
|
||||
);
|
||||
|
||||
@@ -379,7 +379,7 @@ export const licenseServiceFactory = ({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
});
|
||||
}
|
||||
if (instanceType !== InstanceType.OnPrem && instanceType !== InstanceType.EnterpriseOnPremOffline) {
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing`
|
||||
);
|
||||
@@ -407,11 +407,38 @@ export const licenseServiceFactory = ({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
});
|
||||
}
|
||||
if (instanceType !== InstanceType.OnPrem && instanceType !== InstanceType.EnterpriseOnPremOffline) {
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table`
|
||||
);
|
||||
return data;
|
||||
|
||||
const orgMembersUsed = await orgDAL.countAllOrgMembers(orgId);
|
||||
const identityUsed = await identityOrgMembershipDAL.countAllOrgIdentities({ orgId });
|
||||
const projects = await projectDAL.find({ orgId });
|
||||
const projectCount = projects.length;
|
||||
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const { data } = await licenseServerCloudApi.request.get<{
|
||||
head: { name: string }[];
|
||||
rows: { name: string; allowed: boolean }[];
|
||||
}>(`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table`);
|
||||
|
||||
const formattedData = {
|
||||
head: data.head,
|
||||
rows: data.rows.map((el) => {
|
||||
let used = "-";
|
||||
|
||||
if (el.name === BillingPlanRows.MemberLimit.name) {
|
||||
used = orgMembersUsed.toString();
|
||||
} else if (el.name === BillingPlanRows.WorkspaceLimit.name) {
|
||||
used = projectCount.toString();
|
||||
} else if (el.name === BillingPlanRows.IdentityLimit.name) {
|
||||
used = (identityUsed + orgMembersUsed).toString();
|
||||
}
|
||||
|
||||
return {
|
||||
...el,
|
||||
used
|
||||
};
|
||||
})
|
||||
};
|
||||
return formattedData;
|
||||
}
|
||||
|
||||
const mappedRows = await Promise.all(
|
||||
@@ -420,14 +447,11 @@ export const licenseServiceFactory = ({
|
||||
let used = "-";
|
||||
|
||||
if (field === BillingPlanRows.MemberLimit.field) {
|
||||
const orgMemberships = await orgDAL.countAllOrgMembers(orgId);
|
||||
used = orgMemberships.toString();
|
||||
used = orgMembersUsed.toString();
|
||||
} else if (field === BillingPlanRows.WorkspaceLimit.field) {
|
||||
const projects = await projectDAL.find({ orgId });
|
||||
used = projects.length.toString();
|
||||
used = projectCount.toString();
|
||||
} else if (field === BillingPlanRows.IdentityLimit.field) {
|
||||
const identities = await identityOrgMembershipDAL.countAllOrgIdentities({ orgId });
|
||||
used = identities.toString();
|
||||
used = identityUsed.toString();
|
||||
}
|
||||
|
||||
return {
|
||||
|
@@ -171,8 +171,8 @@ export const oidcConfigServiceFactory = ({
|
||||
};
|
||||
|
||||
const oidcLogin = async ({
|
||||
externalId,
|
||||
email,
|
||||
externalId,
|
||||
firstName,
|
||||
lastName,
|
||||
orgId,
|
||||
@@ -714,13 +714,15 @@ export const oidcConfigServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined);
|
||||
|
||||
oidcLogin({
|
||||
email: claims.email,
|
||||
email: claims.email.toLowerCase(),
|
||||
externalId: claims.sub,
|
||||
firstName: claims.given_name ?? "",
|
||||
lastName: claims.family_name ?? "",
|
||||
orgId: org.id,
|
||||
groups: claims.groups as string[] | undefined,
|
||||
groups,
|
||||
callbackPort,
|
||||
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
||||
})
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSecretSyncActions,
|
||||
@@ -76,6 +77,18 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.SshHosts
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionPkiSubscriberActions.Edit,
|
||||
ProjectPermissionPkiSubscriberActions.Read,
|
||||
ProjectPermissionPkiSubscriberActions.Create,
|
||||
ProjectPermissionPkiSubscriberActions.Delete,
|
||||
ProjectPermissionPkiSubscriberActions.IssueCert,
|
||||
ProjectPermissionPkiSubscriberActions.ListCerts
|
||||
],
|
||||
ProjectPermissionSub.PkiSubscribers
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionMemberActions.Create,
|
||||
@@ -113,7 +126,6 @@ const buildAdminPermissionRules = () => {
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
@@ -194,7 +206,6 @@ const buildMemberPermissionRules = () => {
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
@@ -338,6 +349,7 @@ const buildMemberPermissionRules = () => {
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||
|
||||
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
|
||||
can([ProjectPermissionPkiSubscriberActions.Read], ProjectPermissionSub.PkiSubscribers);
|
||||
|
||||
can(
|
||||
[
|
||||
@@ -372,9 +384,10 @@ const buildMemberPermissionRules = () => {
|
||||
const buildViewerPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
|
||||
can(
|
||||
[ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
|
||||
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
||||
|
@@ -41,7 +41,8 @@ export enum OrgPermissionGatewayActions {
|
||||
CreateGateways = "create-gateways",
|
||||
ListGateways = "list-gateways",
|
||||
EditGateways = "edit-gateways",
|
||||
DeleteGateways = "delete-gateways"
|
||||
DeleteGateways = "delete-gateways",
|
||||
AttachGateways = "attach-gateways"
|
||||
}
|
||||
|
||||
export enum OrgPermissionIdentityActions {
|
||||
@@ -337,6 +338,7 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
@@ -378,6 +380,7 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
||||
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
@@ -132,7 +132,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getProjectGroupPermissions = async (projectId: string) => {
|
||||
const getProjectGroupPermissions = async (projectId: string, filterGroupId?: string) => {
|
||||
try {
|
||||
const docs = await db
|
||||
.replicaNode()(TableName.GroupProjectMembership)
|
||||
@@ -148,6 +148,11 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
`groupCustomRoles.id`
|
||||
)
|
||||
.where(`${TableName.GroupProjectMembership}.projectId`, "=", projectId)
|
||||
.where((bd) => {
|
||||
if (filterGroupId) {
|
||||
void bd.where(`${TableName.GroupProjectMembership}.groupId`, "=", filterGroupId);
|
||||
}
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
|
||||
db.ref("id").withSchema(TableName.Groups).as("groupId"),
|
||||
|
@@ -630,6 +630,34 @@ export const permissionServiceFactory = ({
|
||||
return { permission };
|
||||
};
|
||||
|
||||
const checkGroupProjectPermission = async ({
|
||||
groupId,
|
||||
projectId,
|
||||
checkPermissions
|
||||
}: {
|
||||
groupId: string;
|
||||
projectId: string;
|
||||
checkPermissions: ProjectPermissionSet;
|
||||
}) => {
|
||||
const rawGroupProjectPermissions = await permissionDAL.getProjectGroupPermissions(projectId, groupId);
|
||||
const groupPermissions = rawGroupProjectPermissions.map((groupProjectPermission) => {
|
||||
const rolePermissions =
|
||||
groupProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || [];
|
||||
const rules = buildProjectPermissionRules(rolePermissions);
|
||||
const permission = createMongoAbility<ProjectPermissionSet>(rules, {
|
||||
conditionsMatcher
|
||||
});
|
||||
|
||||
return {
|
||||
permission,
|
||||
id: groupProjectPermission.groupId,
|
||||
name: groupProjectPermission.username,
|
||||
membershipId: groupProjectPermission.id
|
||||
};
|
||||
});
|
||||
return groupPermissions.some((groupPermission) => groupPermission.permission.can(...checkPermissions));
|
||||
};
|
||||
|
||||
return {
|
||||
getUserOrgPermission,
|
||||
getOrgPermission,
|
||||
@@ -639,6 +667,7 @@ export const permissionServiceFactory = ({
|
||||
getOrgPermissionByRole,
|
||||
getProjectPermissionByRole,
|
||||
buildOrgPermission,
|
||||
buildProjectPermissionRules
|
||||
buildProjectPermissionRules,
|
||||
checkGroupProjectPermission
|
||||
};
|
||||
};
|
||||
|
@@ -87,6 +87,15 @@ export enum ProjectPermissionSshHostActions {
|
||||
IssueHostCert = "issue-host-cert"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionPkiSubscriberActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
IssueCert = "issue-cert",
|
||||
ListCerts = "list-certs"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretSyncActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
@@ -143,6 +152,7 @@ export enum ProjectPermissionSub {
|
||||
SshCertificateTemplates = "ssh-certificate-templates",
|
||||
SshHosts = "ssh-hosts",
|
||||
SshHostGroups = "ssh-host-groups",
|
||||
PkiSubscribers = "pki-subscribers",
|
||||
PkiAlerts = "pki-alerts",
|
||||
PkiCollections = "pki-collections",
|
||||
Kms = "kms",
|
||||
@@ -190,6 +200,11 @@ export type SshHostSubjectFields = {
|
||||
hostname: string;
|
||||
};
|
||||
|
||||
export type PkiSubscriberSubjectFields = {
|
||||
name: string;
|
||||
// (dangtony98): consider adding [commonName] as a subject field in the future
|
||||
};
|
||||
|
||||
export type ProjectPermissionSet =
|
||||
| [
|
||||
ProjectPermissionSecretActions,
|
||||
@@ -249,6 +264,13 @@ export type ProjectPermissionSet =
|
||||
ProjectPermissionSshHostActions,
|
||||
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
(
|
||||
| ProjectPermissionSub.PkiSubscribers
|
||||
| (ForcedSubject<ProjectPermissionSub.PkiSubscribers> & PkiSubscriberSubjectFields)
|
||||
)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||
@@ -399,6 +421,21 @@ const SshHostConditionSchema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
const PkiSubscriberConditionSchema = z
|
||||
.object({
|
||||
name: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||
})
|
||||
.partial()
|
||||
])
|
||||
})
|
||||
.partial();
|
||||
|
||||
const GeneralPermissionSchema = [
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
||||
@@ -663,6 +700,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.PkiSubscribers).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPkiSubscriberActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
conditions: PkiSubscriberConditionSchema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
|
@@ -342,7 +342,7 @@ export const scimServiceFactory = ({
|
||||
orgMembership = await orgMembershipDAL.create(
|
||||
{
|
||||
userId: userAlias.userId,
|
||||
inviteEmail: email,
|
||||
inviteEmail: email.toLowerCase(),
|
||||
orgId,
|
||||
role,
|
||||
roleId,
|
||||
@@ -364,7 +364,7 @@ export const scimServiceFactory = ({
|
||||
if (trustScimEmails) {
|
||||
user = await userDAL.findOne(
|
||||
{
|
||||
email,
|
||||
email: email.toLowerCase(),
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
@@ -379,8 +379,8 @@ export const scimServiceFactory = ({
|
||||
);
|
||||
user = await userDAL.create(
|
||||
{
|
||||
username: trustScimEmails ? email : uniqueUsername,
|
||||
email,
|
||||
username: trustScimEmails ? email.toLowerCase() : uniqueUsername,
|
||||
email: email.toLowerCase(),
|
||||
isEmailVerified: trustScimEmails,
|
||||
firstName,
|
||||
lastName,
|
||||
@@ -396,7 +396,7 @@ export const scimServiceFactory = ({
|
||||
userId: user.id,
|
||||
aliasType,
|
||||
externalId,
|
||||
emails: email ? [email] : [],
|
||||
emails: email ? [email.toLowerCase()] : [],
|
||||
orgId
|
||||
},
|
||||
tx
|
||||
@@ -418,7 +418,7 @@ export const scimServiceFactory = ({
|
||||
orgMembership = await orgMembershipDAL.create(
|
||||
{
|
||||
userId: user.id,
|
||||
inviteEmail: email,
|
||||
inviteEmail: email.toLowerCase(),
|
||||
orgId,
|
||||
role,
|
||||
roleId,
|
||||
@@ -529,7 +529,7 @@ export const scimServiceFactory = ({
|
||||
membership.userId,
|
||||
{
|
||||
firstName: scimUser.name.givenName,
|
||||
email: scimUser.emails[0].value,
|
||||
email: scimUser.emails[0].value.toLowerCase(),
|
||||
lastName: scimUser.name.familyName,
|
||||
isEmailVerified: hasEmailChanged ? trustScimEmails : undefined
|
||||
},
|
||||
@@ -606,7 +606,7 @@ export const scimServiceFactory = ({
|
||||
membership.userId,
|
||||
{
|
||||
firstName,
|
||||
email,
|
||||
email: email?.toLowerCase(),
|
||||
lastName,
|
||||
isEmailVerified:
|
||||
org.orgAuthMethod === OrgAuthMethod.OIDC ? serverCfg.trustOidcEmails : serverCfg.trustSamlEmails
|
||||
|
@@ -334,7 +334,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecret).as("commitSecretId"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestSecret).as("commitId"),
|
||||
db.raw(
|
||||
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
|
||||
`DENSE_RANK() OVER (PARTITION BY ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."createdAt" DESC) as rank`
|
||||
),
|
||||
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||
@@ -483,7 +483,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitSecretId"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitId"),
|
||||
db.raw(
|
||||
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
|
||||
`DENSE_RANK() OVER (PARTITION BY ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."createdAt" DESC) as rank`
|
||||
),
|
||||
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||
db.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"),
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import ldap from "ldapjs";
|
||||
import ldap, { Client, SearchOptions } from "ldapjs";
|
||||
|
||||
import {
|
||||
TRotationFactory,
|
||||
@@ -8,26 +8,73 @@ import {
|
||||
TRotationFactoryRotateCredentials
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { DistinguishedNameRegex } from "@app/lib/regex";
|
||||
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap";
|
||||
|
||||
import { generatePassword } from "../shared/utils";
|
||||
import {
|
||||
LdapPasswordRotationMethod,
|
||||
TLdapPasswordRotationGeneratedCredentials,
|
||||
TLdapPasswordRotationInput,
|
||||
TLdapPasswordRotationWithConnection
|
||||
} from "./ldap-password-rotation-types";
|
||||
|
||||
const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le");
|
||||
|
||||
const getDN = async (dn: string, client: Client): Promise<string> => {
|
||||
if (DistinguishedNameRegex.test(dn)) return dn;
|
||||
|
||||
const opts: SearchOptions = {
|
||||
filter: `(userPrincipalName=${dn})`,
|
||||
scope: "sub",
|
||||
attributes: ["dn"]
|
||||
};
|
||||
|
||||
const base = dn
|
||||
.split("@")[1]
|
||||
.split(".")
|
||||
.map((dc) => `dc=${dc}`)
|
||||
.join(",");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Perform the search
|
||||
client.search(base, opts, (err, res) => {
|
||||
if (err) {
|
||||
logger.error(err, "LDAP Failed to get DN");
|
||||
reject(new Error(`Provider Resolve DN Error: ${err.message}`));
|
||||
}
|
||||
|
||||
let userDn: string | null;
|
||||
|
||||
res.on("searchEntry", (entry) => {
|
||||
userDn = entry.objectName;
|
||||
});
|
||||
|
||||
res.on("error", (error) => {
|
||||
logger.error(error, "LDAP Failed to get DN");
|
||||
reject(new Error(`Provider Resolve DN Error: ${error.message}`));
|
||||
});
|
||||
|
||||
res.on("end", () => {
|
||||
if (userDn) {
|
||||
resolve(userDn);
|
||||
} else {
|
||||
reject(new Error(`Unable to resolve DN for ${dn}.`));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const ldapPasswordRotationFactory: TRotationFactory<
|
||||
TLdapPasswordRotationWithConnection,
|
||||
TLdapPasswordRotationGeneratedCredentials
|
||||
TLdapPasswordRotationGeneratedCredentials,
|
||||
TLdapPasswordRotationInput["temporaryParameters"]
|
||||
> = (secretRotation, appConnectionDAL, kmsService) => {
|
||||
const {
|
||||
connection,
|
||||
parameters: { dn, passwordRequirements },
|
||||
secretsMapping
|
||||
} = secretRotation;
|
||||
const { connection, parameters, secretsMapping, activeIndex } = secretRotation;
|
||||
|
||||
const { dn, passwordRequirements } = parameters;
|
||||
|
||||
const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => {
|
||||
try {
|
||||
@@ -40,13 +87,21 @@ export const ldapPasswordRotationFactory: TRotationFactory<
|
||||
}
|
||||
};
|
||||
|
||||
const $rotatePassword = async () => {
|
||||
const $rotatePassword = async (currentPassword?: string) => {
|
||||
const { credentials, orgId } = connection;
|
||||
|
||||
if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection");
|
||||
|
||||
const client = await getLdapConnectionClient(credentials);
|
||||
const isPersonalRotation = credentials.dn === dn;
|
||||
const client = await getLdapConnectionClient(
|
||||
currentPassword
|
||||
? {
|
||||
...credentials,
|
||||
password: currentPassword,
|
||||
dn
|
||||
}
|
||||
: credentials
|
||||
);
|
||||
const isConnectionRotation = credentials.dn === dn;
|
||||
|
||||
const password = generatePassword(passwordRequirements);
|
||||
|
||||
@@ -58,8 +113,8 @@ export const ldapPasswordRotationFactory: TRotationFactory<
|
||||
const encodedPassword = getEncodedPassword(password);
|
||||
|
||||
// service account vs personal password rotation require different changes
|
||||
if (isPersonalRotation) {
|
||||
const currentEncodedPassword = getEncodedPassword(credentials.password);
|
||||
if (isConnectionRotation || currentPassword) {
|
||||
const currentEncodedPassword = getEncodedPassword(currentPassword || credentials.password);
|
||||
|
||||
changes = [
|
||||
new ldap.Change({
|
||||
@@ -93,8 +148,9 @@ export const ldapPasswordRotationFactory: TRotationFactory<
|
||||
}
|
||||
|
||||
try {
|
||||
const userDn = await getDN(dn, client);
|
||||
await new Promise((resolve, reject) => {
|
||||
client.modify(dn, changes, (err) => {
|
||||
client.modify(userDn, changes, (err) => {
|
||||
if (err) {
|
||||
logger.error(err, "LDAP Password Rotation Failed");
|
||||
reject(new Error(`Provider Modify Error: ${err.message}`));
|
||||
@@ -110,7 +166,7 @@ export const ldapPasswordRotationFactory: TRotationFactory<
|
||||
|
||||
await $verifyCredentials({ dn, password });
|
||||
|
||||
if (isPersonalRotation) {
|
||||
if (isConnectionRotation) {
|
||||
const updatedCredentials: TLdapConnection["credentials"] = {
|
||||
...credentials,
|
||||
password
|
||||
@@ -128,29 +184,41 @@ export const ldapPasswordRotationFactory: TRotationFactory<
|
||||
return { dn, password };
|
||||
};
|
||||
|
||||
const issueCredentials: TRotationFactoryIssueCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotatePassword();
|
||||
const issueCredentials: TRotationFactoryIssueCredentials<
|
||||
TLdapPasswordRotationGeneratedCredentials,
|
||||
TLdapPasswordRotationInput["temporaryParameters"]
|
||||
> = async (callback, temporaryParameters) => {
|
||||
const credentials = await $rotatePassword(
|
||||
parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal
|
||||
? temporaryParameters?.password
|
||||
: undefined
|
||||
);
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
|
||||
_,
|
||||
credentialsToRevoke,
|
||||
callback
|
||||
) => {
|
||||
const currentPassword = credentialsToRevoke[activeIndex].password;
|
||||
|
||||
// we just rotate to a new password, essentially revoking old credentials
|
||||
await $rotatePassword();
|
||||
await $rotatePassword(
|
||||
parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal ? currentPassword : undefined
|
||||
);
|
||||
|
||||
return callback();
|
||||
};
|
||||
|
||||
const rotateCredentials: TRotationFactoryRotateCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
|
||||
_,
|
||||
callback
|
||||
callback,
|
||||
activeCredentials
|
||||
) => {
|
||||
const credentials = await $rotatePassword();
|
||||
const credentials = await $rotatePassword(
|
||||
parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal ? activeCredentials.password : undefined
|
||||
);
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { LdapPasswordRotationMethod } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-types";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
BaseCreateSecretRotationSchema,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||
import { PasswordRequirementsSchema } from "@app/ee/services/secret-rotation-v2/shared/general";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { DistinguishedNameRegex } from "@app/lib/regex";
|
||||
import { DistinguishedNameRegex, UserPrincipalNameRegex } from "@app/lib/regex";
|
||||
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
@@ -26,10 +26,16 @@ const LdapPasswordRotationParametersSchema = z.object({
|
||||
dn: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
|
||||
.min(1, "Distinguished Name (DN) Required")
|
||||
.min(1, "DN/UPN required")
|
||||
.refine((value) => DistinguishedNameRegex.test(value) || UserPrincipalNameRegex.test(value), {
|
||||
message: "Invalid DN/UPN format"
|
||||
})
|
||||
.describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.dn),
|
||||
passwordRequirements: PasswordRequirementsSchema.optional()
|
||||
passwordRequirements: PasswordRequirementsSchema.optional(),
|
||||
rotationMethod: z
|
||||
.nativeEnum(LdapPasswordRotationMethod)
|
||||
.optional()
|
||||
.describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.rotationMethod)
|
||||
});
|
||||
|
||||
const LdapPasswordRotationSecretsMappingSchema = z.object({
|
||||
@@ -50,10 +56,28 @@ export const LdapPasswordRotationSchema = BaseSecretRotationSchema(SecretRotatio
|
||||
secretsMapping: LdapPasswordRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword).extend({
|
||||
parameters: LdapPasswordRotationParametersSchema,
|
||||
secretsMapping: LdapPasswordRotationSecretsMappingSchema
|
||||
});
|
||||
export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword)
|
||||
.extend({
|
||||
parameters: LdapPasswordRotationParametersSchema,
|
||||
secretsMapping: LdapPasswordRotationSecretsMappingSchema,
|
||||
temporaryParameters: z
|
||||
.object({
|
||||
password: z.string().min(1, "Password required").describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.password)
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.superRefine((val, ctx) => {
|
||||
if (
|
||||
val.parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal &&
|
||||
!val.temporaryParameters?.password
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Password required",
|
||||
path: ["temporaryParameters", "password"]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const UpdateLdapPasswordRotationSchema = BaseUpdateSecretRotationSchema(SecretRotation.LdapPassword).extend({
|
||||
parameters: LdapPasswordRotationParametersSchema.optional(),
|
||||
|
@@ -9,6 +9,11 @@ import {
|
||||
LdapPasswordRotationSchema
|
||||
} from "./ldap-password-rotation-schemas";
|
||||
|
||||
export enum LdapPasswordRotationMethod {
|
||||
ConnectionPrincipal = "connection-principal",
|
||||
TargetPrincipal = "target-principal"
|
||||
}
|
||||
|
||||
export type TLdapPasswordRotation = z.infer<typeof LdapPasswordRotationSchema>;
|
||||
|
||||
export type TLdapPasswordRotationInput = z.infer<typeof CreateLdapPasswordRotationSchema>;
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
|
||||
import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
|
||||
import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret";
|
||||
import { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password";
|
||||
import { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } from "./ldap-password";
|
||||
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
||||
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
||||
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
|
||||
@@ -15,7 +16,8 @@ import {
|
||||
TSecretRotationV2,
|
||||
TSecretRotationV2GeneratedCredentials,
|
||||
TSecretRotationV2ListItem,
|
||||
TSecretRotationV2Raw
|
||||
TSecretRotationV2Raw,
|
||||
TUpdateSecretRotationV2DTO
|
||||
} from "./secret-rotation-v2-types";
|
||||
|
||||
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
|
||||
@@ -228,3 +230,30 @@ export const parseRotationErrorMessage = (err: unknown): string => {
|
||||
? errorMessage
|
||||
: `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
|
||||
};
|
||||
|
||||
function haveUnequalProperties<T>(obj1: T, obj2: T, properties: (keyof T)[]): boolean {
|
||||
return properties.some((prop) => obj1[prop] !== obj2[prop]);
|
||||
}
|
||||
|
||||
export const throwOnImmutableParameterUpdate = (
|
||||
updatePayload: TUpdateSecretRotationV2DTO,
|
||||
secretRotation: TSecretRotationV2Raw
|
||||
) => {
|
||||
if (!updatePayload.parameters) return;
|
||||
|
||||
switch (updatePayload.type) {
|
||||
case SecretRotation.LdapPassword:
|
||||
if (
|
||||
haveUnequalProperties(
|
||||
updatePayload.parameters as TLdapPasswordRotation["parameters"],
|
||||
secretRotation.parameters as TLdapPasswordRotation["parameters"],
|
||||
["rotationMethod", "dn"]
|
||||
)
|
||||
) {
|
||||
throw new BadRequestError({ message: "Cannot update rotation method or DN" });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
};
|
||||
|
@@ -25,7 +25,8 @@ import {
|
||||
getNextUtcRotationInterval,
|
||||
getSecretRotationRotateSecretJobOptions,
|
||||
listSecretRotationOptions,
|
||||
parseRotationErrorMessage
|
||||
parseRotationErrorMessage,
|
||||
throwOnImmutableParameterUpdate
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-fns";
|
||||
import {
|
||||
SECRET_ROTATION_CONNECTION_MAP,
|
||||
@@ -46,6 +47,7 @@ import {
|
||||
TSecretRotationV2,
|
||||
TSecretRotationV2GeneratedCredentials,
|
||||
TSecretRotationV2Raw,
|
||||
TSecretRotationV2TemporaryParameters,
|
||||
TSecretRotationV2WithConnection,
|
||||
TUpdateSecretRotationV2DTO
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
@@ -112,7 +114,8 @@ const MAX_GENERATED_CREDENTIALS_LENGTH = 2;
|
||||
|
||||
type TRotationFactoryImplementation = TRotationFactory<
|
||||
TSecretRotationV2WithConnection,
|
||||
TSecretRotationV2GeneratedCredentials
|
||||
TSecretRotationV2GeneratedCredentials,
|
||||
TSecretRotationV2TemporaryParameters
|
||||
>;
|
||||
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
|
||||
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||
@@ -400,6 +403,7 @@ export const secretRotationV2ServiceFactory = ({
|
||||
environment,
|
||||
rotateAtUtc = { hours: 0, minutes: 0 },
|
||||
secretsMapping,
|
||||
temporaryParameters,
|
||||
...payload
|
||||
}: TCreateSecretRotationV2DTO,
|
||||
actor: OrgServiceActor
|
||||
@@ -546,7 +550,7 @@ export const secretRotationV2ServiceFactory = ({
|
||||
|
||||
return createdRotation;
|
||||
});
|
||||
});
|
||||
}, temporaryParameters);
|
||||
|
||||
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
|
||||
await snapshotService.performSnapshot(folder.id);
|
||||
@@ -585,10 +589,7 @@ export const secretRotationV2ServiceFactory = ({
|
||||
}
|
||||
};
|
||||
|
||||
const updateSecretRotation = async (
|
||||
{ type, rotationId, ...payload }: TUpdateSecretRotationV2DTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const updateSecretRotation = async (dto: TUpdateSecretRotationV2DTO, actor: OrgServiceActor) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.secretRotation)
|
||||
@@ -596,6 +597,8 @@ export const secretRotationV2ServiceFactory = ({
|
||||
message: "Failed to update secret rotation due to plan restriction. Upgrade plan to update secret rotations."
|
||||
});
|
||||
|
||||
const { type, rotationId, ...payload } = dto;
|
||||
|
||||
const secretRotation = await secretRotationV2DAL.findById(rotationId);
|
||||
|
||||
if (!secretRotation)
|
||||
@@ -603,6 +606,8 @@ export const secretRotationV2ServiceFactory = ({
|
||||
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID ${rotationId}`
|
||||
});
|
||||
|
||||
throwOnImmutableParameterUpdate(dto, secretRotation);
|
||||
|
||||
const { folder, environment, projectId, folderId, connection } = secretRotation;
|
||||
const secretsMapping = secretRotation.secretsMapping as TSecretRotationV2["secretsMapping"];
|
||||
|
||||
@@ -877,6 +882,7 @@ export const secretRotationV2ServiceFactory = ({
|
||||
const inactiveIndex = (activeIndex + 1) % MAX_GENERATED_CREDENTIALS_LENGTH;
|
||||
|
||||
const inactiveCredentials = generatedCredentials[inactiveIndex];
|
||||
const activeCredentials = generatedCredentials[activeIndex];
|
||||
|
||||
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation](
|
||||
{
|
||||
@@ -887,73 +893,77 @@ export const secretRotationV2ServiceFactory = ({
|
||||
kmsService
|
||||
);
|
||||
|
||||
const updatedRotation = await rotationFactory.rotateCredentials(inactiveCredentials, async (newCredentials) => {
|
||||
const updatedCredentials = [...generatedCredentials];
|
||||
updatedCredentials[inactiveIndex] = newCredentials;
|
||||
const updatedRotation = await rotationFactory.rotateCredentials(
|
||||
inactiveCredentials,
|
||||
async (newCredentials) => {
|
||||
const updatedCredentials = [...generatedCredentials];
|
||||
updatedCredentials[inactiveIndex] = newCredentials;
|
||||
|
||||
const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({
|
||||
projectId,
|
||||
generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials,
|
||||
kmsService
|
||||
});
|
||||
|
||||
return secretRotationV2DAL.transaction(async (tx) => {
|
||||
const secretsPayload = rotationFactory.getSecretsPayload(newCredentials);
|
||||
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({
|
||||
projectId,
|
||||
generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials,
|
||||
kmsService
|
||||
});
|
||||
|
||||
// update mapped secrets with new credential values
|
||||
await fnSecretBulkUpdate({
|
||||
folderId,
|
||||
orgId: connection.orgId,
|
||||
tx,
|
||||
inputSecrets: secretsPayload.map(({ key, value }) => ({
|
||||
filter: {
|
||||
key,
|
||||
folderId,
|
||||
type: SecretType.Shared
|
||||
},
|
||||
data: {
|
||||
encryptedValue: encryptor({
|
||||
plainText: Buffer.from(value)
|
||||
}).cipherTextBlob,
|
||||
references: []
|
||||
}
|
||||
})),
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||
secretTagDAL,
|
||||
resourceMetadataDAL
|
||||
});
|
||||
return secretRotationV2DAL.transaction(async (tx) => {
|
||||
const secretsPayload = rotationFactory.getSecretsPayload(newCredentials);
|
||||
|
||||
const currentTime = new Date();
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
return secretRotationV2DAL.updateById(
|
||||
secretRotation.id,
|
||||
{
|
||||
encryptedGeneratedCredentials: encryptedUpdatedCredentials,
|
||||
activeIndex: inactiveIndex,
|
||||
isLastRotationManual: isManualRotation,
|
||||
lastRotatedAt: currentTime,
|
||||
lastRotationAttemptedAt: currentTime,
|
||||
nextRotationAt: calculateNextRotationAt({
|
||||
...(secretRotation as TSecretRotationV2),
|
||||
rotationStatus: SecretRotationStatus.Success,
|
||||
// update mapped secrets with new credential values
|
||||
await fnSecretBulkUpdate({
|
||||
folderId,
|
||||
orgId: connection.orgId,
|
||||
tx,
|
||||
inputSecrets: secretsPayload.map(({ key, value }) => ({
|
||||
filter: {
|
||||
key,
|
||||
folderId,
|
||||
type: SecretType.Shared
|
||||
},
|
||||
data: {
|
||||
encryptedValue: encryptor({
|
||||
plainText: Buffer.from(value)
|
||||
}).cipherTextBlob,
|
||||
references: []
|
||||
}
|
||||
})),
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||
secretTagDAL,
|
||||
resourceMetadataDAL
|
||||
});
|
||||
|
||||
const currentTime = new Date();
|
||||
|
||||
return secretRotationV2DAL.updateById(
|
||||
secretRotation.id,
|
||||
{
|
||||
encryptedGeneratedCredentials: encryptedUpdatedCredentials,
|
||||
activeIndex: inactiveIndex,
|
||||
isLastRotationManual: isManualRotation,
|
||||
lastRotatedAt: currentTime,
|
||||
isManualRotation
|
||||
}),
|
||||
rotationStatus: SecretRotationStatus.Success,
|
||||
lastRotationJobId: jobId,
|
||||
encryptedLastRotationMessage: null
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
});
|
||||
lastRotationAttemptedAt: currentTime,
|
||||
nextRotationAt: calculateNextRotationAt({
|
||||
...(secretRotation as TSecretRotationV2),
|
||||
rotationStatus: SecretRotationStatus.Success,
|
||||
lastRotatedAt: currentTime,
|
||||
isManualRotation
|
||||
}),
|
||||
rotationStatus: SecretRotationStatus.Success,
|
||||
lastRotationJobId: jobId,
|
||||
encryptedLastRotationMessage: null
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
},
|
||||
activeCredentials
|
||||
);
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
...(auditLogInfo ?? {
|
||||
|
@@ -87,6 +87,8 @@ export type TSecretRotationV2ListItem =
|
||||
| TLdapPasswordRotationListItem
|
||||
| TAwsIamUserSecretRotationListItem;
|
||||
|
||||
export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined;
|
||||
|
||||
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
|
||||
|
||||
export type TListSecretRotationsV2ByProjectId = {
|
||||
@@ -120,6 +122,7 @@ export type TCreateSecretRotationV2DTO = Pick<
|
||||
environment: string;
|
||||
isAutoRotationEnabled?: boolean;
|
||||
rotateAtUtc?: TRotateAtUtc;
|
||||
temporaryParameters?: TSecretRotationV2TemporaryParameters;
|
||||
};
|
||||
|
||||
export type TUpdateSecretRotationV2DTO = Partial<
|
||||
@@ -186,8 +189,12 @@ export type TSecretRotationSendNotificationJobPayload = {
|
||||
// transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the
|
||||
// third party credential changes (when supported), preventing credentials getting out of sync
|
||||
|
||||
export type TRotationFactoryIssueCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>
|
||||
export type TRotationFactoryIssueCredentials<
|
||||
T extends TSecretRotationV2GeneratedCredentials,
|
||||
P extends TSecretRotationV2TemporaryParameters = undefined
|
||||
> = (
|
||||
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>,
|
||||
temporaryParameters?: P
|
||||
) => Promise<TSecretRotationV2Raw>;
|
||||
|
||||
export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||
@@ -197,7 +204,8 @@ export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2Generat
|
||||
|
||||
export type TRotationFactoryRotateCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||
credentialsToRevoke: T[number] | undefined,
|
||||
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>
|
||||
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>,
|
||||
activeCredentials: T[number]
|
||||
) => Promise<TSecretRotationV2Raw>;
|
||||
|
||||
export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||
@@ -206,13 +214,14 @@ export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2Generat
|
||||
|
||||
export type TRotationFactory<
|
||||
T extends TSecretRotationV2WithConnection,
|
||||
C extends TSecretRotationV2GeneratedCredentials
|
||||
C extends TSecretRotationV2GeneratedCredentials,
|
||||
P extends TSecretRotationV2TemporaryParameters = undefined
|
||||
> = (
|
||||
secretRotation: T,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
issueCredentials: TRotationFactoryIssueCredentials<C>;
|
||||
issueCredentials: TRotationFactoryIssueCredentials<C, P>;
|
||||
revokeCredentials: TRotationFactoryRevokeCredentials<C>;
|
||||
rotateCredentials: TRotationFactoryRotateCredentials<C>;
|
||||
getSecretsPayload: TRotationFactoryGetSecretsPayload<C>;
|
||||
|
@@ -28,6 +28,7 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||
.where(`${TableName.SshHostGroup}.projectId`, projectId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
|
||||
@@ -35,7 +36,8 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
|
||||
db.ref("name").withSchema(TableName.SshHostGroup),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping)
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||
)
|
||||
.orderBy(`${TableName.SshHostGroup}.updatedAt`, "desc");
|
||||
|
||||
@@ -69,7 +71,8 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
|
||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||
}
|
||||
}));
|
||||
return {
|
||||
@@ -99,6 +102,7 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||
.where(`${TableName.SshHostGroup}.id`, sshHostGroupId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHostGroup).as("sshHostGroupId"),
|
||||
@@ -106,7 +110,8 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
|
||||
db.ref("name").withSchema(TableName.SshHostGroup),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping)
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||
);
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
@@ -121,7 +126,8 @@ export const sshHostGroupDALFactory = (db: TDbClient) => {
|
||||
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||
}
|
||||
}));
|
||||
|
||||
|
@@ -12,6 +12,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TGroupDALFactory } from "../group/group-dal";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { createSshLoginMappings } from "../ssh-host/ssh-host-fns";
|
||||
import {
|
||||
@@ -43,8 +44,12 @@ type TSshHostGroupServiceFactoryDep = {
|
||||
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction" | "delete">;
|
||||
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
|
||||
userDAL: Pick<TUserDALFactory, "find">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
||||
permissionService: Pick<
|
||||
TPermissionServiceFactory,
|
||||
"getProjectPermission" | "getUserProjectPermission" | "checkGroupProjectPermission"
|
||||
>;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
groupDAL: Pick<TGroupDALFactory, "findGroupsByProjectId">;
|
||||
};
|
||||
|
||||
export type TSshHostGroupServiceFactory = ReturnType<typeof sshHostGroupServiceFactory>;
|
||||
@@ -58,7 +63,8 @@ export const sshHostGroupServiceFactory = ({
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
licenseService,
|
||||
groupDAL
|
||||
}: TSshHostGroupServiceFactoryDep) => {
|
||||
const createSshHostGroup = async ({
|
||||
projectId,
|
||||
@@ -127,6 +133,7 @@ export const sshHostGroupServiceFactory = ({
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
groupDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId,
|
||||
@@ -179,13 +186,42 @@ export const sshHostGroupServiceFactory = ({
|
||||
});
|
||||
|
||||
const updatedSshHostGroup = await sshHostGroupDAL.transaction(async (tx) => {
|
||||
await sshHostGroupDAL.updateById(
|
||||
sshHostGroupId,
|
||||
{
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (name && name !== sshHostGroup.name) {
|
||||
// (dangtony98): room to optimize check to ensure that
|
||||
// the SSH host group name is unique across the whole org
|
||||
const project = await projectDAL.findById(sshHostGroup.projectId, tx);
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${sshHostGroup.projectId}' not found` });
|
||||
const projects = await projectDAL.find(
|
||||
{
|
||||
orgId: project.orgId
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
const existingSshHostGroup = await sshHostGroupDAL.find(
|
||||
{
|
||||
name,
|
||||
$in: {
|
||||
projectId: projects.map((p) => p.id)
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
if (existingSshHostGroup.length) {
|
||||
throw new BadRequestError({
|
||||
message: `SSH host group with name '${name}' already exists in the organization`
|
||||
});
|
||||
}
|
||||
await sshHostGroupDAL.updateById(
|
||||
sshHostGroupId,
|
||||
{
|
||||
name
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (loginMappings) {
|
||||
await sshHostLoginUserDAL.delete({ sshHostGroupId: sshHostGroup.id }, tx);
|
||||
if (loginMappings.length) {
|
||||
@@ -194,6 +230,7 @@ export const sshHostGroupServiceFactory = ({
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
groupDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId: sshHostGroup.projectId,
|
||||
|
@@ -9,12 +9,7 @@ export type TCreateSshHostGroupDTO = {
|
||||
export type TUpdateSshHostGroupDTO = {
|
||||
sshHostGroupId: string;
|
||||
name?: string;
|
||||
loginMappings?: {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
};
|
||||
}[];
|
||||
loginMappings?: TLoginMapping[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetSshHostGroupDTO = {
|
||||
|
@@ -31,8 +31,18 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SshHostLoginUser}.id`,
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SshHostLoginUserMapping}.userId`)
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.UserGroupMembership}.groupId`,
|
||||
`${TableName.SshHostLoginUserMapping}.groupId`
|
||||
)
|
||||
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
|
||||
.andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
||||
.andWhere((bd) => {
|
||||
void bd
|
||||
.where(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
||||
.orWhere(`${TableName.UserGroupMembership}.userId`, userId);
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||
db.ref("projectId").withSchema(TableName.SshHost),
|
||||
@@ -58,8 +68,17 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.join(TableName.SshHost, `${TableName.SshHostGroupMembership}.sshHostId`, `${TableName.SshHost}.id`)
|
||||
.leftJoin(
|
||||
TableName.UserGroupMembership,
|
||||
`${TableName.UserGroupMembership}.groupId`,
|
||||
`${TableName.SshHostLoginUserMapping}.groupId`
|
||||
)
|
||||
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
|
||||
.andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
||||
.andWhere((bd) => {
|
||||
void bd
|
||||
.where(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
||||
.orWhere(`${TableName.UserGroupMembership}.userId`, userId);
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||
db.ref("projectId").withSchema(TableName.SshHost),
|
||||
@@ -133,6 +152,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||
.where(`${TableName.SshHost}.projectId`, projectId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||
@@ -144,6 +164,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||
db.ref("slug").withSchema(TableName.Groups).as("groupSlug"),
|
||||
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||
)
|
||||
@@ -163,10 +184,12 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||
.select(
|
||||
db.ref("sshHostId").withSchema(TableName.SshHostGroupMembership),
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users)
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||
)
|
||||
.whereIn(`${TableName.SshHostGroupMembership}.sshHostId`, hostIds);
|
||||
|
||||
@@ -185,7 +208,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
const directMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||
},
|
||||
source: LoginMappingSource.HOST
|
||||
}));
|
||||
@@ -197,7 +221,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
const groupMappings = Object.entries(inheritedGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||
},
|
||||
source: LoginMappingSource.HOST_GROUP
|
||||
}));
|
||||
@@ -229,6 +254,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||
.where(`${TableName.SshHost}.id`, sshHostId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||
@@ -241,7 +267,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||
db.ref("hostSshCaId").withSchema(TableName.SshHost),
|
||||
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||
);
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
@@ -257,7 +284,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
const directMappings = Object.entries(directGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||
},
|
||||
source: LoginMappingSource.HOST
|
||||
}));
|
||||
@@ -275,10 +303,12 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||
)
|
||||
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin(TableName.Groups, `${TableName.SshHostLoginUserMapping}.groupId`, `${TableName.Groups}.id`)
|
||||
.where(`${TableName.SshHostGroupMembership}.sshHostId`, sshHostId)
|
||||
.select(
|
||||
db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
|
||||
db.ref("username").withSchema(TableName.Users)
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("slug").withSchema(TableName.Groups).as("groupSlug")
|
||||
);
|
||||
|
||||
const groupGrouped = groupBy(
|
||||
@@ -289,7 +319,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
||||
const groupMappings = Object.entries(groupGrouped).map(([loginUser, entries]) => ({
|
||||
loginUser,
|
||||
allowedPrincipals: {
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||
usernames: unique(entries.map((e) => e.username)).filter(Boolean),
|
||||
groups: unique(entries.map((e) => e.groupSlug)).filter(Boolean)
|
||||
},
|
||||
source: LoginMappingSource.HOST_GROUP
|
||||
}));
|
||||
|
@@ -3,6 +3,7 @@ import { Knex } from "knex";
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TCreateSshLoginMappingsDTO } from "./ssh-host-types";
|
||||
|
||||
/**
|
||||
@@ -15,6 +16,7 @@ export const createSshLoginMappings = async ({
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
groupDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId,
|
||||
@@ -35,7 +37,7 @@ export const createSshLoginMappings = async ({
|
||||
tx
|
||||
);
|
||||
|
||||
if (allowedPrincipals.usernames.length > 0) {
|
||||
if (allowedPrincipals.usernames && allowedPrincipals.usernames.length > 0) {
|
||||
const users = await userDAL.find(
|
||||
{
|
||||
$in: {
|
||||
@@ -74,6 +76,41 @@ export const createSshLoginMappings = async ({
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (allowedPrincipals.groups && allowedPrincipals.groups.length > 0) {
|
||||
const projectGroups = await groupDAL.findGroupsByProjectId(projectId);
|
||||
const groups = projectGroups.filter((g) => allowedPrincipals.groups?.includes(g.slug));
|
||||
|
||||
if (groups.length !== allowedPrincipals.groups?.length) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid group slugs: ${allowedPrincipals.groups
|
||||
.filter((g) => !projectGroups.some((pg) => pg.slug === g))
|
||||
.join(", ")}`
|
||||
});
|
||||
}
|
||||
|
||||
for await (const group of groups) {
|
||||
// check that each group has access to the SSH project and have read access to hosts
|
||||
const hasPermission = await permissionService.checkGroupProjectPermission({
|
||||
groupId: group.id,
|
||||
projectId,
|
||||
checkPermissions: [ProjectPermissionSshHostActions.Read, ProjectPermissionSub.SshHosts]
|
||||
});
|
||||
if (!hasPermission) {
|
||||
throw new BadRequestError({
|
||||
message: `Group ${group.slug} does not have access to the SSH project`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await sshHostLoginUserMappingDAL.insertMany(
|
||||
groups.map((group) => ({
|
||||
sshHostLoginUserId: sshHostLoginUser.id,
|
||||
groupId: group.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -15,7 +15,24 @@ export const sanitizedSshHost = SshHostsSchema.pick({
|
||||
|
||||
export const loginMappingSchema = z.object({
|
||||
loginUser: z.string().trim(),
|
||||
allowedPrincipals: z.object({
|
||||
usernames: z.array(z.string().trim()).transform((usernames) => Array.from(new Set(usernames)))
|
||||
})
|
||||
allowedPrincipals: z
|
||||
.object({
|
||||
usernames: z
|
||||
.array(z.string().trim())
|
||||
.transform((usernames) => Array.from(new Set(usernames)))
|
||||
.optional(),
|
||||
groups: z
|
||||
.array(z.string().trim())
|
||||
.transform((groups) => Array.from(new Set(groups)))
|
||||
.optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
return (data.usernames && data.usernames.length > 0) || (data.groups && data.groups.length > 0);
|
||||
},
|
||||
{
|
||||
message: "At least one username or group must be provided",
|
||||
path: ["allowedPrincipals"]
|
||||
}
|
||||
)
|
||||
});
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||
@@ -19,6 +20,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TUserGroupMembershipDALFactory } from "../group/user-group-membership-dal";
|
||||
import {
|
||||
convertActorToPrincipals,
|
||||
createSshCert,
|
||||
@@ -39,12 +41,14 @@ import {
|
||||
|
||||
type TSshHostServiceFactoryDep = {
|
||||
userDAL: Pick<TUserDALFactory, "findById" | "find">;
|
||||
groupDAL: Pick<TGroupDALFactory, "findGroupsByProjectId">;
|
||||
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">;
|
||||
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findOne">;
|
||||
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create" | "transaction">;
|
||||
sshCertificateBodyDAL: Pick<TSshCertificateBodyDALFactory, "create">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findGroupMembershipsByUserIdInOrg">;
|
||||
sshHostDAL: Pick<
|
||||
TSshHostDALFactory,
|
||||
| "transaction"
|
||||
@@ -58,7 +62,10 @@ type TSshHostServiceFactoryDep = {
|
||||
>;
|
||||
sshHostLoginUserDAL: TSshHostLoginUserDALFactory;
|
||||
sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
||||
permissionService: Pick<
|
||||
TPermissionServiceFactory,
|
||||
"getProjectPermission" | "getUserProjectPermission" | "checkGroupProjectPermission"
|
||||
>;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
@@ -66,6 +73,8 @@ export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
|
||||
|
||||
export const sshHostServiceFactory = ({
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupDAL,
|
||||
projectDAL,
|
||||
projectSshConfigDAL,
|
||||
sshCertificateAuthorityDAL,
|
||||
@@ -208,6 +217,7 @@ export const sshHostServiceFactory = ({
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
groupDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId,
|
||||
@@ -278,6 +288,7 @@ export const sshHostServiceFactory = ({
|
||||
loginMappings,
|
||||
sshHostLoginUserDAL,
|
||||
sshHostLoginUserMappingDAL,
|
||||
groupDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectId: host.projectId,
|
||||
@@ -324,7 +335,7 @@ export const sshHostServiceFactory = ({
|
||||
return host;
|
||||
};
|
||||
|
||||
const getSshHost = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => {
|
||||
const getSshHostById = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => {
|
||||
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
|
||||
if (!host) {
|
||||
throw new NotFoundError({
|
||||
@@ -387,10 +398,14 @@ export const sshHostServiceFactory = ({
|
||||
userDAL
|
||||
});
|
||||
|
||||
const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actorId, actorOrgId);
|
||||
const userGroupSlugs = userGroups.map((g) => g.groupSlug);
|
||||
|
||||
const mapping = host.loginMappings.find(
|
||||
(m) =>
|
||||
m.loginUser === loginUser &&
|
||||
m.allowedPrincipals.usernames.some((allowed) => internalPrincipals.includes(allowed))
|
||||
(m.allowedPrincipals.usernames?.some((allowed) => internalPrincipals.includes(allowed)) ||
|
||||
m.allowedPrincipals.groups?.some((allowed) => userGroupSlugs.includes(allowed)))
|
||||
);
|
||||
|
||||
if (!mapping) {
|
||||
@@ -616,7 +631,7 @@ export const sshHostServiceFactory = ({
|
||||
createSshHost,
|
||||
updateSshHost,
|
||||
deleteSshHost,
|
||||
getSshHost,
|
||||
getSshHostById,
|
||||
issueSshHostUserCert,
|
||||
issueSshHostHostCert,
|
||||
getSshHostUserCaPk,
|
||||
|
@@ -7,12 +7,15 @@ import { TProjectPermission } from "@app/lib/types";
|
||||
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TGroupDALFactory } from "../group/group-dal";
|
||||
|
||||
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TLoginMapping = {
|
||||
loginUser: string;
|
||||
allowedPrincipals: {
|
||||
usernames: string[];
|
||||
usernames?: string[];
|
||||
groups?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -63,7 +66,8 @@ type BaseCreateSshLoginMappingsDTO = {
|
||||
sshHostLoginUserDAL: Pick<TSshHostLoginUserDALFactory, "create" | "transaction">;
|
||||
sshHostLoginUserMappingDAL: Pick<TSshHostLoginUserMappingDALFactory, "insertMany">;
|
||||
userDAL: Pick<TUserDALFactory, "find">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getUserProjectPermission">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getUserProjectPermission" | "checkGroupProjectPermission">;
|
||||
groupDAL: Pick<TGroupDALFactory, "findGroupsByProjectId">;
|
||||
projectId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
|
@@ -14,6 +14,7 @@ export enum ApiDocsTags {
|
||||
UniversalAuth = "Universal Auth",
|
||||
GcpAuth = "GCP Auth",
|
||||
AwsAuth = "AWS Auth",
|
||||
OciAuth = "OCI Auth",
|
||||
AzureAuth = "Azure Auth",
|
||||
KubernetesAuth = "Kubernetes Auth",
|
||||
JwtAuth = "JWT Auth",
|
||||
@@ -46,6 +47,7 @@ export enum ApiDocsTags {
|
||||
PkiCertificateTemplates = "PKI Certificate Templates",
|
||||
PkiCertificateCollections = "PKI Certificate Collections",
|
||||
PkiAlerting = "PKI Alerting",
|
||||
PkiSubscribers = "PKI Subscribers",
|
||||
SshCertificates = "SSH Certificates",
|
||||
SshCertificateAuthorities = "SSH Certificate Authorities",
|
||||
SshCertificateTemplates = "SSH Certificate Templates",
|
||||
@@ -270,6 +272,40 @@ export const AWS_AUTH = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const OCI_AUTH = {
|
||||
LOGIN: {
|
||||
identityId: "The ID of the identity to login.",
|
||||
userOcid: "The OCID of the user attempting login.",
|
||||
headers: "The headers of the signed request."
|
||||
},
|
||||
ATTACH: {
|
||||
identityId: "The ID of the identity to attach the configuration onto.",
|
||||
tenancyOcid: "The OCID of your tenancy.",
|
||||
allowedUsernames:
|
||||
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
|
||||
accessTokenTTL: "The lifetime for an access token in seconds.",
|
||||
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
|
||||
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.",
|
||||
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
|
||||
},
|
||||
UPDATE: {
|
||||
identityId: "The ID of the identity to update the auth method for.",
|
||||
tenancyOcid: "The OCID of your tenancy.",
|
||||
allowedUsernames:
|
||||
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
|
||||
accessTokenTTL: "The new lifetime for an access token in seconds.",
|
||||
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
|
||||
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
|
||||
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
|
||||
},
|
||||
RETRIEVE: {
|
||||
identityId: "The ID of the identity to retrieve the auth method for."
|
||||
},
|
||||
REVOKE: {
|
||||
identityId: "The ID of the identity to revoke the auth method for."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const AZURE_AUTH = {
|
||||
LOGIN: {
|
||||
identityId: "The ID of the identity to login."
|
||||
@@ -357,6 +393,7 @@ export const KUBERNETES_AUTH = {
|
||||
allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.",
|
||||
allowedAudience:
|
||||
"The optional audience claim that the service account JWT token must have to authenticate with Infisical.",
|
||||
gatewayId: "The ID of the gateway to use when performing kubernetes API requests.",
|
||||
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
|
||||
accessTokenTTL: "The lifetime for an access token in seconds.",
|
||||
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
|
||||
@@ -373,6 +410,7 @@ export const KUBERNETES_AUTH = {
|
||||
allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.",
|
||||
allowedAudience:
|
||||
"The new optional audience claim that the service account JWT token must have to authenticate with Infisical.",
|
||||
gatewayId: "The ID of the gateway to use when performing kubernetes API requests.",
|
||||
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
|
||||
accessTokenTTL: "The new lifetime for an acccess token in seconds.",
|
||||
accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.",
|
||||
@@ -570,7 +608,8 @@ export const PROJECTS = {
|
||||
projectDescription: "An optional description label for the project.",
|
||||
autoCapitalization: "Disable or enable auto-capitalization for the project.",
|
||||
slug: "An optional slug for the project. (must be unique within the organization)",
|
||||
hasDeleteProtection: "Enable or disable delete protection for the project."
|
||||
hasDeleteProtection: "Enable or disable delete protection for the project.",
|
||||
secretSharing: "Enable or disable secret sharing for the project."
|
||||
},
|
||||
GET_KEY: {
|
||||
workspaceId: "The ID of the project to get the key from."
|
||||
@@ -639,6 +678,9 @@ export const PROJECTS = {
|
||||
commonName: "The common name of the certificate to filter by.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th certificate.",
|
||||
limit: "The number of certificates to return."
|
||||
},
|
||||
LIST_PKI_SUBSCRIBERS: {
|
||||
projectId: "The ID of the project to list PKI subscribers for."
|
||||
}
|
||||
} as const;
|
||||
|
||||
@@ -1478,7 +1520,7 @@ export const SSH_HOSTS = {
|
||||
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
||||
allowedPrincipals: "A list of allowed principals that can log in as the login user.",
|
||||
loginMappings:
|
||||
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project.",
|
||||
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users or groups slugs in the Infisical SSH project.",
|
||||
userSshCaId:
|
||||
"The ID of the SSH CA to use for user certificates. If not specified, the default user SSH CA will be used if it exists.",
|
||||
hostSshCaId:
|
||||
@@ -1493,7 +1535,7 @@ export const SSH_HOSTS = {
|
||||
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
||||
allowedPrincipals: "A list of allowed principals that can log in as the login user.",
|
||||
loginMappings:
|
||||
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project."
|
||||
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users or groups slugs in the Infisical SSH project."
|
||||
},
|
||||
DELETE: {
|
||||
sshHostId: "The ID of the SSH host to delete."
|
||||
@@ -1731,6 +1773,67 @@ export const ALERTS = {
|
||||
}
|
||||
};
|
||||
|
||||
export const PKI_SUBSCRIBERS = {
|
||||
GET: {
|
||||
subscriberName: "The name of the PKI subscriber to get.",
|
||||
projectId: "The ID of the project to get the PKI subscriber for."
|
||||
},
|
||||
CREATE: {
|
||||
projectId: "The ID of the project to create the PKI subscriber in.",
|
||||
caId: "The ID of the CA that will issue certificates for the PKI subscriber.",
|
||||
name: "The name of the PKI subscriber.",
|
||||
commonName: "The common name (CN) to be used on certificates issued for this subscriber.",
|
||||
status: "The status of the PKI subscriber. This can be one of active or disabled.",
|
||||
ttl: "The time to live for the certificates issued for this subscriber such as 1m, 1h, 1d, 1y, ...",
|
||||
subjectAlternativeNames:
|
||||
"A list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.",
|
||||
keyUsages: "The key usage extension to be used on certificates issued for this subscriber.",
|
||||
extendedKeyUsages: "The extended key usage extension to be used on certificates issued for this subscriber."
|
||||
},
|
||||
UPDATE: {
|
||||
projectId: "The ID of the project to update the PKI subscriber in.",
|
||||
subscriberName: "The name of the PKI subscriber to update.",
|
||||
caId: "The ID of the CA that will issue certificates for the PKI subscriber to update to.",
|
||||
name: "The name of the PKI subscriber to update to.",
|
||||
commonName: "The common name (CN) to be used on certificates issued for this subscriber to update to.",
|
||||
status: "The status of the PKI subscriber to update to. This can be one of active or disabled.",
|
||||
ttl: "The time to live for the certificates issued for this subscriber such as 1m, 1h, 1d, 1y, ...",
|
||||
subjectAlternativeNames:
|
||||
"A comma-delimited list of Subject Alternative Names (SANs) to be used on certificates issued for this subscriber; these can be host names or email addresses.",
|
||||
keyUsages: "The key usage extension to be used on certificates issued for this subscriber to update to.",
|
||||
extendedKeyUsages:
|
||||
"The extended key usage extension to be used on certificates issued for this subscriber to update to."
|
||||
},
|
||||
DELETE: {
|
||||
subscriberName: "The name of the PKI subscriber to delete.",
|
||||
projectId: "The ID of the project of the PKI subscriber to delete."
|
||||
},
|
||||
ISSUE_CERT: {
|
||||
subscriberName: "The name of the PKI subscriber to issue the certificate for.",
|
||||
projectId: "The ID of the project of the PKI subscriber to issue the certificate for.",
|
||||
certificate: "The issued certificate.",
|
||||
issuingCaCertificate: "The certificate of the issuing CA.",
|
||||
certificateChain: "The certificate chain of the issued certificate.",
|
||||
privateKey: "The private key of the issued certificate.",
|
||||
serialNumber: "The serial number of the issued certificate."
|
||||
},
|
||||
SIGN_CERT: {
|
||||
subscriberName: "The name of the PKI subscriber to sign the certificate for.",
|
||||
projectId: "The ID of the project of the PKI subscriber to sign the certificate for.",
|
||||
csr: "The CSR to be used to sign the certificate.",
|
||||
certificate: "The signed certificate.",
|
||||
issuingCaCertificate: "The certificate of the issuing CA.",
|
||||
certificateChain: "The certificate chain of the signed certificate.",
|
||||
serialNumber: "The serial number of the signed certificate."
|
||||
},
|
||||
LIST_CERTS: {
|
||||
subscriberName: "The name of the PKI subscriber to list the certificates for.",
|
||||
projectId: "The ID of the project of the PKI subscriber to list the certificates for.",
|
||||
offset: "The offset to start from.",
|
||||
limit: "The number of certificates to return."
|
||||
}
|
||||
};
|
||||
|
||||
export const PKI_COLLECTIONS = {
|
||||
CREATE: {
|
||||
projectId: "The ID of the project to create the PKI collection in.",
|
||||
@@ -1960,7 +2063,7 @@ export const AppConnections = {
|
||||
LDAP: {
|
||||
provider: "The type of LDAP provider. Determines provider-specific behaviors.",
|
||||
url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').",
|
||||
dn: "The Distinguished Name (DN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com').",
|
||||
dn: "The Distinguished Name (DN) or User Principal Name (UPN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com').",
|
||||
password: "The password to bind with for authentication.",
|
||||
sslRejectUnauthorized:
|
||||
"Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.",
|
||||
@@ -1974,6 +2077,13 @@ export const AppConnections = {
|
||||
AZURE_CLIENT_SECRETS: {
|
||||
code: "The OAuth code to use to connect with Azure Client Secrets.",
|
||||
tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
|
||||
},
|
||||
OCI: {
|
||||
userOcid: "The OCID (Oracle Cloud Identifier) of the user making the request.",
|
||||
tenancyOcid: "The OCID (Oracle Cloud Identifier) of the tenancy in Oracle Cloud Infrastructure.",
|
||||
region: "The region identifier in Oracle Cloud Infrastructure where the vault is located.",
|
||||
fingerprint: "The fingerprint of the public key uploaded to the user's API keys.",
|
||||
privateKey: "The private key content in PEM format used to sign API requests."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2037,6 +2147,7 @@ export const SecretSyncs = {
|
||||
const destinationName = SECRET_SYNC_NAME_MAP[destination];
|
||||
return {
|
||||
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`,
|
||||
keySchema: `Specify the format to use for structuring secret keys in the ${destinationName} destination.`,
|
||||
disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.`
|
||||
};
|
||||
},
|
||||
@@ -2121,6 +2232,11 @@ export const SecretSyncs = {
|
||||
TEAMCITY: {
|
||||
project: "The TeamCity project to sync secrets to.",
|
||||
buildConfig: "The TeamCity build configuration to sync secrets to."
|
||||
},
|
||||
OCI_VAULT: {
|
||||
compartmentOcid: "The OCID (Oracle Cloud Identifier) of the compartment where the vault is located.",
|
||||
vaultOcid: "The OCID (Oracle Cloud Identifier) of the vault to sync secrets to.",
|
||||
keyOcid: "The OCID (Oracle Cloud Identifier) of the encryption key to use when creating secrets in the vault."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2192,7 +2308,10 @@ export const SecretRotations = {
|
||||
clientId: "The client ID of the Azure Application to rotate the client secret for."
|
||||
},
|
||||
LDAP_PASSWORD: {
|
||||
dn: "The Distinguished Name (DN) of the principal to rotate the password for."
|
||||
dn: "The Distinguished Name (DN) or User Principal Name (UPN) of the principal to rotate the password for.",
|
||||
rotationMethod:
|
||||
'Whether the rotation should be performed by the LDAP "connection-principal" or the "target-principal" (defaults to \'connection-principal\').',
|
||||
password: 'The password of the provided principal if "parameters.rotationMethod" is set to "target-principal".'
|
||||
},
|
||||
GENERAL: {
|
||||
PASSWORD_REQUIREMENTS: {
|
||||
@@ -2226,7 +2345,7 @@ export const SecretRotations = {
|
||||
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
|
||||
},
|
||||
LDAP_PASSWORD: {
|
||||
dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.",
|
||||
dn: "The name of the secret that the Distinguished Name (DN) or User Principal Name (UPN) of the principal will be mapped to.",
|
||||
password: "The name of the secret that the rotated password will be mapped to."
|
||||
},
|
||||
AWS_IAM_USER_SECRET: {
|
||||
|
@@ -174,6 +174,8 @@ const setupProxyServer = async ({
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
|
||||
let streamClosed = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
server.on("connection", async (clientConn) => {
|
||||
try {
|
||||
@@ -202,9 +204,15 @@ const setupProxyServer = async ({
|
||||
|
||||
// Handle client connection close
|
||||
clientConn.on("end", () => {
|
||||
writer.close().catch((err) => {
|
||||
logger.error(err);
|
||||
});
|
||||
if (!streamClosed) {
|
||||
try {
|
||||
writer.close().catch((err) => {
|
||||
logger.debug(err, "Error closing writer (already closed)");
|
||||
});
|
||||
} catch (error) {
|
||||
logger.debug(error, "Error in writer close");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
clientConn.on("error", (clientConnErr) => {
|
||||
@@ -249,14 +257,29 @@ const setupProxyServer = async ({
|
||||
setupCopy();
|
||||
// Handle connection closure
|
||||
clientConn.on("close", () => {
|
||||
stream.destroy().catch((err) => {
|
||||
proxyErrorMsg.push((err as Error)?.message);
|
||||
});
|
||||
if (!streamClosed) {
|
||||
streamClosed = true;
|
||||
stream.destroy().catch((err) => {
|
||||
logger.debug(err, "Stream already destroyed during close event");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const cleanup = async () => {
|
||||
clientConn?.destroy();
|
||||
await stream.destroy();
|
||||
try {
|
||||
clientConn?.destroy();
|
||||
} catch (err) {
|
||||
logger.debug(err, "Error destroying client connection");
|
||||
}
|
||||
|
||||
if (!streamClosed) {
|
||||
streamClosed = true;
|
||||
try {
|
||||
await stream.destroy();
|
||||
} catch (err) {
|
||||
logger.debug(err, "Error destroying stream (might be already closed)");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
clientConn.on("error", (clientConnErr) => {
|
||||
@@ -301,8 +324,17 @@ const setupProxyServer = async ({
|
||||
server,
|
||||
port: address.port,
|
||||
cleanup: async () => {
|
||||
server.close();
|
||||
await quicClient?.destroy();
|
||||
try {
|
||||
server.close();
|
||||
} catch (err) {
|
||||
logger.debug(err, "Error closing server");
|
||||
}
|
||||
|
||||
try {
|
||||
await quicClient?.destroy();
|
||||
} catch (err) {
|
||||
logger.debug(err, "Error destroying QUIC client");
|
||||
}
|
||||
},
|
||||
getProxyError: () => proxyErrorMsg.join(",")
|
||||
});
|
||||
@@ -320,10 +352,10 @@ interface ProxyOptions {
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
export const withGatewayProxy = async (
|
||||
callback: (port: number) => Promise<void>,
|
||||
export const withGatewayProxy = async <T>(
|
||||
callback: (port: number) => Promise<T>,
|
||||
options: ProxyOptions
|
||||
): Promise<void> => {
|
||||
): Promise<T> => {
|
||||
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
|
||||
|
||||
// Setup the proxy server
|
||||
@@ -339,7 +371,7 @@ export const withGatewayProxy = async (
|
||||
|
||||
try {
|
||||
// Execute the callback with the allocated port
|
||||
await callback(port);
|
||||
return await callback(port);
|
||||
} catch (err) {
|
||||
const proxyErrorMessage = getProxyError();
|
||||
if (proxyErrorMessage) {
|
||||
|
@@ -32,13 +32,13 @@ export const buildFindFilter =
|
||||
<R extends object = object>(
|
||||
{ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>,
|
||||
tableName?: TableName,
|
||||
excludeKeys?: Array<keyof R>
|
||||
excludeKeys?: string[]
|
||||
) =>
|
||||
(bd: Knex.QueryBuilder<R, R>) => {
|
||||
const processedFilter = tableName
|
||||
? Object.fromEntries(
|
||||
Object.entries(filter)
|
||||
.filter(([key]) => !excludeKeys || !excludeKeys.includes(key as keyof R))
|
||||
.filter(([key]) => !excludeKeys || !excludeKeys.includes(key))
|
||||
.map(([key, value]) => [`${tableName}.${key}`, value])
|
||||
)
|
||||
: filter;
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { Knex } from "knex";
|
||||
import { Compare, Filter, parse } from "scim2-parse-filter";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => {
|
||||
if (filter.op !== "[]" && filter.op !== "and" && filter.op !== "or" && filter.op !== "not") {
|
||||
return { ...filter, attrPath: `${parentPath}.${(filter as Compare).attrPath}` };
|
||||
@@ -27,8 +29,12 @@ const processDynamicQuery = (
|
||||
const { scimFilterAst, query } = stack.pop()!;
|
||||
switch (scimFilterAst.op) {
|
||||
case "eq": {
|
||||
let sanitizedValue = scimFilterAst.compValue;
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.where(attrPath, scimFilterAst.compValue);
|
||||
if (attrPath === `${TableName.Users}.email` && typeof sanitizedValue === "string") {
|
||||
sanitizedValue = sanitizedValue.toLowerCase();
|
||||
}
|
||||
if (attrPath) void query.where(attrPath, sanitizedValue);
|
||||
break;
|
||||
}
|
||||
case "pr": {
|
||||
@@ -62,18 +68,30 @@ const processDynamicQuery = (
|
||||
break;
|
||||
}
|
||||
case "ew": {
|
||||
let sanitizedValue = scimFilterAst.compValue;
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}`);
|
||||
if (attrPath === `${TableName.Users}.email` && typeof sanitizedValue === "string") {
|
||||
sanitizedValue = sanitizedValue.toLowerCase();
|
||||
}
|
||||
if (attrPath) void query.whereILike(attrPath, `%${sanitizedValue}`);
|
||||
break;
|
||||
}
|
||||
case "co": {
|
||||
let sanitizedValue = scimFilterAst.compValue;
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}%`);
|
||||
if (attrPath === `${TableName.Users}.email` && typeof sanitizedValue === "string") {
|
||||
sanitizedValue = sanitizedValue.toLowerCase();
|
||||
}
|
||||
if (attrPath) void query.whereILike(attrPath, `%${sanitizedValue}%`);
|
||||
break;
|
||||
}
|
||||
case "ne": {
|
||||
let sanitizedValue = scimFilterAst.compValue;
|
||||
const attrPath = getAttributeField(scimFilterAst.attrPath);
|
||||
if (attrPath) void query.whereNot(attrPath, "=", scimFilterAst.compValue);
|
||||
if (attrPath === `${TableName.Users}.email` && typeof sanitizedValue === "string") {
|
||||
sanitizedValue = sanitizedValue.toLowerCase();
|
||||
}
|
||||
if (attrPath) void query.whereNot(attrPath, "=", sanitizedValue);
|
||||
break;
|
||||
}
|
||||
case "and": {
|
||||
|
@@ -1,3 +1,11 @@
|
||||
import RE2 from "re2";
|
||||
|
||||
export const DistinguishedNameRegex =
|
||||
// DN format, ie; CN=user,OU=users,DC=example,DC=com
|
||||
/^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/;
|
||||
new RE2(
|
||||
/^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/
|
||||
);
|
||||
|
||||
export const UserPrincipalNameRegex = new RE2(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9._-]+\.[a-zA-Z]{2,}$/);
|
||||
|
||||
export const LdapUrlRegex = new RE2(/^ldaps?:\/\//);
|
||||
|
@@ -5,7 +5,7 @@
|
||||
import type { FastifySchema, FastifySchemaCompiler, FastifyTypeProvider } from "fastify";
|
||||
import type { FastifySerializerCompiler } from "fastify/types/schema";
|
||||
import type { z, ZodAny, ZodTypeAny } from "zod";
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
import { PostProcessCallback, zodToJsonSchema } from "zod-to-json-schema";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type FreeformRecord = Record<string, any>;
|
||||
@@ -28,9 +28,25 @@ interface Schema extends FastifySchema {
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
// Credit: https://github.com/StefanTerdell/zod-to-json-schema
|
||||
const jsonDescription: PostProcessCallback = (jsonSchema, def) => {
|
||||
if (def.description) {
|
||||
try {
|
||||
return {
|
||||
...jsonSchema,
|
||||
description: undefined,
|
||||
...JSON.parse(def.description)
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return jsonSchema;
|
||||
};
|
||||
|
||||
const zodToJsonSchemaOptions = {
|
||||
target: "openApi3",
|
||||
$refStrategy: "none"
|
||||
$refStrategy: "none",
|
||||
postProcess: jsonDescription
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@@ -57,7 +57,9 @@ export const registerServeUI = async (
|
||||
reply.callNotFound();
|
||||
return;
|
||||
}
|
||||
return reply.sendFile("index.html");
|
||||
// reference: https://github.com/fastify/fastify-static?tab=readme-ov-file#managing-cache-control-headers
|
||||
// to avoid ui bundle skew on new deployment
|
||||
return reply.sendFile("index.html", { maxAge: 0, immutable: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -32,7 +32,6 @@ import { externalKmsServiceFactory } from "@app/ee/services/external-kms/externa
|
||||
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 { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal";
|
||||
import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
|
||||
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
@@ -162,6 +161,8 @@ import { identityKubernetesAuthDALFactory } from "@app/services/identity-kuberne
|
||||
import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
|
||||
import { identityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal";
|
||||
import { identityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service";
|
||||
import { identityOciAuthDALFactory } from "@app/services/identity-oci-auth/identity-oci-auth-dal";
|
||||
import { identityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
|
||||
import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal";
|
||||
import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
|
||||
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
@@ -197,6 +198,8 @@ import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-servic
|
||||
import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||
import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
||||
import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
|
||||
import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
|
||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||
import { projectQueueFactory } from "@app/services/project/project-queue";
|
||||
import { projectServiceFactory } from "@app/services/project/project-service";
|
||||
@@ -353,6 +356,7 @@ export const registerRoutes = async (
|
||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
|
||||
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
|
||||
const identityOciAuthDAL = identityOciAuthDALFactory(db);
|
||||
const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
|
||||
const identityJwtAuthDAL = identityJwtAuthDALFactory(db);
|
||||
const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
|
||||
@@ -434,7 +438,6 @@ export const registerRoutes = async (
|
||||
|
||||
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
|
||||
const gatewayDAL = gatewayDALFactory(db);
|
||||
const projectGatewayDAL = projectGatewayDALFactory(db);
|
||||
const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db);
|
||||
const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
|
||||
|
||||
@@ -622,7 +625,6 @@ export const registerRoutes = async (
|
||||
|
||||
const userService = userServiceFactory({
|
||||
userDAL,
|
||||
userAliasDAL,
|
||||
orgMembershipDAL,
|
||||
tokenService,
|
||||
permissionService,
|
||||
@@ -828,6 +830,7 @@ export const registerRoutes = async (
|
||||
const pkiAlertDAL = pkiAlertDALFactory(db);
|
||||
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
||||
const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db);
|
||||
const pkiSubscriberDAL = pkiSubscriberDALFactory(db);
|
||||
|
||||
const certificateService = certificateServiceFactory({
|
||||
certificateDAL,
|
||||
@@ -870,6 +873,8 @@ export const registerRoutes = async (
|
||||
|
||||
const sshHostService = sshHostServiceFactory({
|
||||
userDAL,
|
||||
groupDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectDAL,
|
||||
projectSshConfigDAL,
|
||||
sshCertificateAuthorityDAL,
|
||||
@@ -892,7 +897,8 @@ export const registerRoutes = async (
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
licenseService,
|
||||
groupDAL
|
||||
});
|
||||
|
||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||
@@ -959,6 +965,20 @@ export const registerRoutes = async (
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const pkiSubscriberService = pkiSubscriberServiceFactory({
|
||||
pkiSubscriberDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const projectTemplateService = projectTemplateServiceFactory({
|
||||
licenseService,
|
||||
permissionService,
|
||||
@@ -1056,6 +1076,7 @@ export const registerRoutes = async (
|
||||
projectRoleDAL,
|
||||
folderDAL,
|
||||
licenseService,
|
||||
pkiSubscriberDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
pkiAlertDAL,
|
||||
@@ -1398,12 +1419,24 @@ export const registerRoutes = async (
|
||||
identityUaDAL,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const gatewayService = gatewayServiceFactory({
|
||||
permissionService,
|
||||
gatewayDAL,
|
||||
kmsService,
|
||||
licenseService,
|
||||
orgGatewayConfigDAL,
|
||||
keyStore
|
||||
});
|
||||
|
||||
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
|
||||
identityKubernetesAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
gatewayService,
|
||||
gatewayDAL,
|
||||
kmsService
|
||||
});
|
||||
const identityGcpAuthService = identityGcpAuthServiceFactory({
|
||||
@@ -1430,6 +1463,14 @@ export const registerRoutes = async (
|
||||
licenseService
|
||||
});
|
||||
|
||||
const identityOciAuthService = identityOciAuthServiceFactory({
|
||||
identityAccessTokenDAL,
|
||||
identityOciAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
licenseService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const identityOidcAuthService = identityOidcAuthServiceFactory({
|
||||
identityOidcAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
@@ -1458,16 +1499,6 @@ export const registerRoutes = async (
|
||||
identityDAL
|
||||
});
|
||||
|
||||
const gatewayService = gatewayServiceFactory({
|
||||
permissionService,
|
||||
gatewayDAL,
|
||||
kmsService,
|
||||
licenseService,
|
||||
orgGatewayConfigDAL,
|
||||
keyStore,
|
||||
projectGatewayDAL
|
||||
});
|
||||
|
||||
const dynamicSecretProviders = buildDynamicSecretProviders({
|
||||
gatewayService
|
||||
});
|
||||
@@ -1489,7 +1520,7 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService,
|
||||
projectGatewayDAL,
|
||||
gatewayDAL,
|
||||
resourceMetadataDAL
|
||||
});
|
||||
|
||||
@@ -1716,6 +1747,7 @@ export const registerRoutes = async (
|
||||
identityGcpAuth: identityGcpAuthService,
|
||||
identityAwsAuth: identityAwsAuthService,
|
||||
identityAzureAuth: identityAzureAuthService,
|
||||
identityOciAuth: identityOciAuthService,
|
||||
identityOidcAuth: identityOidcAuthService,
|
||||
identityJwtAuth: identityJwtAuthService,
|
||||
identityLdapAuth: identityLdapAuthService,
|
||||
@@ -1742,6 +1774,7 @@ export const registerRoutes = async (
|
||||
certificateEst: certificateEstService,
|
||||
pkiAlert: pkiAlertService,
|
||||
pkiCollection: pkiCollectionService,
|
||||
pkiSubscriber: pkiSubscriberService,
|
||||
secretScanning: secretScanningService,
|
||||
license: licenseService,
|
||||
trustedIp: trustedIpService,
|
||||
@@ -1784,6 +1817,10 @@ export const registerRoutes = async (
|
||||
if (licenseSyncJob) {
|
||||
cronJobs.push(licenseSyncJob);
|
||||
}
|
||||
const microsoftTeamsSyncJob = await microsoftTeamsService.initializeBackgroundSync();
|
||||
if (microsoftTeamsSyncJob) {
|
||||
cronJobs.push(microsoftTeamsSyncJob);
|
||||
}
|
||||
}
|
||||
|
||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||
|
@@ -261,7 +261,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
|
||||
pitVersionLimit: true,
|
||||
kmsCertificateKeyId: true,
|
||||
auditLogsRetentionDays: true,
|
||||
hasDeleteProtection: true
|
||||
hasDeleteProtection: true,
|
||||
secretSharing: true
|
||||
});
|
||||
|
||||
export const SanitizedTagSchema = SecretTagsSchema.pick({
|
||||
|
@@ -38,6 +38,7 @@ import {
|
||||
} from "@app/services/app-connection/humanitec";
|
||||
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
|
||||
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
|
||||
import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/services/app-connection/oci";
|
||||
import {
|
||||
PostgresConnectionListItemSchema,
|
||||
SanitizedPostgresConnectionSchema
|
||||
@@ -76,7 +77,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedAzureClientSecretsConnectionSchema.options,
|
||||
...SanitizedWindmillConnectionSchema.options,
|
||||
...SanitizedLdapConnectionSchema.options,
|
||||
...SanitizedTeamCityConnectionSchema.options
|
||||
...SanitizedTeamCityConnectionSchema.options,
|
||||
...SanitizedOCIConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -97,7 +99,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
AzureClientSecretsConnectionListItemSchema,
|
||||
WindmillConnectionListItemSchema,
|
||||
LdapConnectionListItemSchema,
|
||||
TeamCityConnectionListItemSchema
|
||||
TeamCityConnectionListItemSchema,
|
||||
OCIConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
@@ -13,6 +13,7 @@ import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router";
|
||||
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
|
||||
import { registerLdapConnectionRouter } from "./ldap-connection-router";
|
||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||
import { registerOCIConnectionRouter } from "./oci-connection-router";
|
||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
|
||||
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
|
||||
@@ -40,5 +41,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.Auth0]: registerAuth0ConnectionRouter,
|
||||
[AppConnection.HCVault]: registerHCVaultConnectionRouter,
|
||||
[AppConnection.LDAP]: registerLdapConnectionRouter,
|
||||
[AppConnection.TeamCity]: registerTeamCityConnectionRouter
|
||||
[AppConnection.TeamCity]: registerTeamCityConnectionRouter,
|
||||
[AppConnection.OCI]: registerOCIConnectionRouter
|
||||
};
|
||||
|
@@ -0,0 +1,123 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateOCIConnectionSchema,
|
||||
SanitizedOCIConnectionSchema,
|
||||
UpdateOCIConnectionSchema
|
||||
} from "@app/services/app-connection/oci";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerOCIConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.OCI,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedOCIConnectionSchema,
|
||||
createSchema: CreateOCIConnectionSchema,
|
||||
updateSchema: UpdateOCIConnectionSchema
|
||||
});
|
||||
|
||||
// The following endpoints are for internal Infisical App use only and not part of the public API
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/compartments`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
|
||||
const compartments = await server.services.appConnection.oci.listCompartments(connectionId, req.permission);
|
||||
return compartments;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/vaults`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
compartmentOcid: z.string().min(1, "Compartment OCID required")
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
displayName: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const { compartmentOcid } = req.query;
|
||||
|
||||
const vaults = await server.services.appConnection.oci.listVaults(
|
||||
{ connectionId, compartmentOcid },
|
||||
req.permission
|
||||
);
|
||||
return vaults;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/vault-keys`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
compartmentOcid: z.string().min(1, "Compartment OCID required"),
|
||||
vaultOcid: z.string().min(1, "Vault OCID required")
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
displayName: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const { compartmentOcid, vaultOcid } = req.query;
|
||||
|
||||
const keys = await server.services.appConnection.oci.listVaultKeys(
|
||||
{ connectionId, compartmentOcid, vaultOcid },
|
||||
req.permission
|
||||
);
|
||||
return keys;
|
||||
}
|
||||
});
|
||||
};
|
@@ -131,8 +131,8 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
||||
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||
privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey),
|
||||
certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||
privateKey: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.privateKey),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
||||
})
|
||||
}
|
||||
@@ -518,7 +518,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
|
||||
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||
certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain),
|
||||
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
|
||||
})
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
import { IdentityKubernetesAuthsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
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";
|
||||
@@ -21,7 +22,8 @@ const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick(
|
||||
kubernetesHost: true,
|
||||
allowedNamespaces: true,
|
||||
allowedNames: true,
|
||||
allowedAudience: true
|
||||
allowedAudience: true,
|
||||
gatewayId: true
|
||||
}).extend({
|
||||
caCert: z.string(),
|
||||
tokenReviewerJwt: z.string().optional().nullable()
|
||||
@@ -100,12 +102,32 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost),
|
||||
kubernetesHost: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.describe(KUBERNETES_AUTH.ATTACH.kubernetesHost)
|
||||
.refine(
|
||||
(val) =>
|
||||
characterValidator([
|
||||
CharacterType.Alphabets,
|
||||
CharacterType.Numbers,
|
||||
CharacterType.Colon,
|
||||
CharacterType.Period,
|
||||
CharacterType.ForwardSlash,
|
||||
CharacterType.Hyphen
|
||||
])(val),
|
||||
{
|
||||
message:
|
||||
"Kubernetes host must only contain alphabets, numbers, colons, periods, hyphen, and forward slashes."
|
||||
}
|
||||
),
|
||||
caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert),
|
||||
tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt),
|
||||
allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation
|
||||
allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames),
|
||||
allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience),
|
||||
gatewayId: z.string().uuid().optional().nullable().describe(KUBERNETES_AUTH.ATTACH.gatewayId),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
@@ -199,12 +221,36 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost),
|
||||
kubernetesHost: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(KUBERNETES_AUTH.UPDATE.kubernetesHost)
|
||||
.refine(
|
||||
(val) => {
|
||||
if (!val) return true;
|
||||
|
||||
return characterValidator([
|
||||
CharacterType.Alphabets,
|
||||
CharacterType.Numbers,
|
||||
CharacterType.Colon,
|
||||
CharacterType.Period,
|
||||
CharacterType.ForwardSlash,
|
||||
CharacterType.Hyphen
|
||||
])(val);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Kubernetes host must only contain alphabets, numbers, colons, periods, hyphen, and forward slashes."
|
||||
}
|
||||
),
|
||||
caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert),
|
||||
tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt),
|
||||
allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation
|
||||
allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames),
|
||||
allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience),
|
||||
gatewayId: z.string().uuid().optional().nullable().describe(KUBERNETES_AUTH.UPDATE.gatewayId),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
|
338
backend/src/server/routes/v1/identity-oci-auth-router.ts
Normal file
338
backend/src/server/routes/v1/identity-oci-auth-router.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityOciAuthsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, OCI_AUTH } from "@app/lib/api-docs";
|
||||
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 { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||
import { validateTenancy, validateUsernames } from "@app/services/identity-oci-auth/identity-oci-auth-validators";
|
||||
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
|
||||
|
||||
export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/oci-auth/login",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.OciAuth],
|
||||
description: "Login with OCI Auth",
|
||||
body: z.object({
|
||||
identityId: z.string().trim().describe(OCI_AUTH.LOGIN.identityId),
|
||||
userOcid: z.string().trim().describe(OCI_AUTH.LOGIN.userOcid),
|
||||
headers: z
|
||||
.object({
|
||||
authorization: z.string(),
|
||||
host: z.string(),
|
||||
"x-date": z.string()
|
||||
})
|
||||
.describe(OCI_AUTH.LOGIN.headers)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
accessToken: z.string(),
|
||||
expiresIn: z.coerce.number(),
|
||||
accessTokenMaxTTL: z.coerce.number(),
|
||||
tokenType: z.literal("Bearer")
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityOciAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
await server.services.identityOciAuth.login(req.body);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg?.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_OCI_AUTH,
|
||||
metadata: {
|
||||
identityId: identityOciAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
identityOciAuthId: identityOciAuth.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
tokenType: "Bearer" as const,
|
||||
expiresIn: identityOciAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/oci-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.OciAuth],
|
||||
description: "Attach OCI Auth configuration onto identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim().describe(OCI_AUTH.ATTACH.identityId)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
tenancyOcid: validateTenancy.describe(OCI_AUTH.ATTACH.tenancyOcid),
|
||||
allowedUsernames: validateUsernames.describe(OCI_AUTH.ATTACH.allowedUsernames),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||
.describe(OCI_AUTH.ATTACH.accessTokenTrustedIps),
|
||||
accessTokenTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(OCI_AUTH.ATTACH.accessTokenTTL),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(OCI_AUTH.ATTACH.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OCI_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||
})
|
||||
.refine(
|
||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityOciAuth: IdentityOciAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityOciAuth = await server.services.identityOciAuth.attachOciAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
identityId: req.params.identityId,
|
||||
isActorSuperAdmin: isSuperAdmin(req.auth)
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityOciAuth.orgId,
|
||||
event: {
|
||||
type: EventType.ADD_IDENTITY_OCI_AUTH,
|
||||
metadata: {
|
||||
identityId: identityOciAuth.identityId,
|
||||
tenancyOcid: identityOciAuth.tenancyOcid,
|
||||
allowedUsernames: identityOciAuth.allowedUsernames || null,
|
||||
accessTokenTTL: identityOciAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityOciAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityOciAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/oci-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.OciAuth],
|
||||
description: "Update OCI Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(OCI_AUTH.UPDATE.identityId)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
tenancyOcid: validateTenancy.describe(OCI_AUTH.UPDATE.tenancyOcid),
|
||||
allowedUsernames: validateUsernames.describe(OCI_AUTH.UPDATE.allowedUsernames),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(OCI_AUTH.UPDATE.accessTokenTrustedIps),
|
||||
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(OCI_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(OCI_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe(OCI_AUTH.UPDATE.accessTokenMaxTTL)
|
||||
})
|
||||
.refine(
|
||||
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityOciAuth: IdentityOciAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityOciAuth = await server.services.identityOciAuth.updateOciAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
identityId: req.params.identityId,
|
||||
allowedUsernames: req.body.allowedUsernames || null
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityOciAuth.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_IDENTITY_OCI_AUTH,
|
||||
metadata: {
|
||||
identityId: identityOciAuth.identityId,
|
||||
tenancyOcid: identityOciAuth.tenancyOcid,
|
||||
allowedUsernames: identityOciAuth.allowedUsernames || null,
|
||||
accessTokenTTL: identityOciAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityOciAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityOciAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/oci-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.OciAuth],
|
||||
description: "Retrieve OCI Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(OCI_AUTH.RETRIEVE.identityId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityOciAuth: IdentityOciAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityOciAuth = await server.services.identityOciAuth.getOciAuth({
|
||||
identityId: req.params.identityId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityOciAuth.orgId,
|
||||
event: {
|
||||
type: EventType.GET_IDENTITY_OCI_AUTH,
|
||||
metadata: {
|
||||
identityId: identityOciAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
return { identityOciAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/oci-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.OciAuth],
|
||||
description: "Delete OCI Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(OCI_AUTH.REVOKE.identityId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityOciAuth: IdentityOciAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityOciAuth = await server.services.identityOciAuth.revokeIdentityOciAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
identityId: req.params.identityId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityOciAuth.orgId,
|
||||
event: {
|
||||
type: EventType.REVOKE_IDENTITY_OCI_AUTH,
|
||||
metadata: {
|
||||
identityId: identityOciAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityOciAuth };
|
||||
}
|
||||
});
|
||||
};
|
@@ -52,7 +52,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
identity: IdentitiesSchema.extend({
|
||||
authMethods: z.array(z.string())
|
||||
authMethods: z.array(z.string()),
|
||||
metadata: z.object({ id: z.string(), key: z.string(), value: z.string() }).array()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -123,7 +124,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identity: IdentitiesSchema
|
||||
identity: IdentitiesSchema.extend({
|
||||
metadata: z.object({ id: z.string(), key: z.string(), value: z.string() }).array()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -227,8 +230,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
identity: IdentityOrgMembershipsSchema.extend({
|
||||
metadata: z
|
||||
.object({
|
||||
key: z.string().trim().min(1),
|
||||
id: z.string().trim().min(1),
|
||||
key: z.string().trim().min(1),
|
||||
value: z.string().trim().min(1)
|
||||
})
|
||||
.array()
|
||||
|
@@ -20,6 +20,7 @@ import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
|
||||
import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router";
|
||||
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
|
||||
import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
|
||||
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
|
||||
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
|
||||
import { registerIdentityRouter } from "./identity-router";
|
||||
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
|
||||
@@ -33,6 +34,7 @@ import { registerOrgRouter } from "./organization-router";
|
||||
import { registerPasswordRouter } from "./password-router";
|
||||
import { registerPkiAlertRouter } from "./pki-alert-router";
|
||||
import { registerPkiCollectionRouter } from "./pki-collection-router";
|
||||
import { registerPkiSubscriberRouter } from "./pki-subscriber-router";
|
||||
import { registerProjectEnvRouter } from "./project-env-router";
|
||||
import { registerProjectKeyRouter } from "./project-key-router";
|
||||
import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||
@@ -62,6 +64,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await authRouter.register(registerIdentityAccessTokenRouter);
|
||||
await authRouter.register(registerIdentityAwsAuthRouter);
|
||||
await authRouter.register(registerIdentityAzureAuthRouter);
|
||||
await authRouter.register(registerIdentityOciAuthRouter);
|
||||
await authRouter.register(registerIdentityOidcAuthRouter);
|
||||
await authRouter.register(registerIdentityJwtAuthRouter);
|
||||
await authRouter.register(registerIdentityLdapAuthRouter);
|
||||
@@ -105,6 +108,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
||||
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
|
||||
await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" });
|
||||
},
|
||||
{ prefix: "/pki" }
|
||||
);
|
||||
|
@@ -16,7 +16,12 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
inviteeEmails: z.array(z.string().trim().email()),
|
||||
inviteeEmails: z
|
||||
.string()
|
||||
.trim()
|
||||
.email()
|
||||
.array()
|
||||
.refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"),
|
||||
organizationId: z.string().trim(),
|
||||
projects: z
|
||||
.object({
|
||||
@@ -115,7 +120,11 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
email: z.string().trim().email(),
|
||||
email: z
|
||||
.string()
|
||||
.trim()
|
||||
.email()
|
||||
.refine((val) => val === val.toLowerCase(), "Email must be lowercase"),
|
||||
organizationId: z.string().trim(),
|
||||
code: z.string().trim()
|
||||
}),
|
||||
|
@@ -275,6 +275,23 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
{ message: "Duration value must be at least 1" }
|
||||
)
|
||||
.optional(),
|
||||
secretsProductEnabled: z.boolean().optional(),
|
||||
pkiProductEnabled: z.boolean().optional(),
|
||||
kmsProductEnabled: z.boolean().optional(),
|
||||
sshProductEnabled: z.boolean().optional(),
|
||||
scannerProductEnabled: z.boolean().optional(),
|
||||
shareSecretsProductEnabled: z.boolean().optional(),
|
||||
maxSharedSecretLifetime: z
|
||||
.number()
|
||||
.min(300, "Max Shared Secret lifetime cannot be under 5 minutes")
|
||||
.max(2592000, "Max Shared Secret lifetime cannot exceed 30 days")
|
||||
.optional(),
|
||||
maxSharedSecretViewLimit: z
|
||||
.number()
|
||||
.min(1, "Max Shared Secret view count cannot be lower than 1")
|
||||
.max(1000, "Max Shared Secret view count cannot exceed 1000")
|
||||
.nullable()
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
|
478
backend/src/server/routes/v1/pki-subscriber-router.ts
Normal file
478
backend/src/server/routes/v1/pki-subscriber-router.ts
Normal file
@@ -0,0 +1,478 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificatesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, PKI_SUBSCRIBERS } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { validateAltNameField } from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema";
|
||||
import { PkiSubscriberStatus } from "@app/services/pki-subscriber/pki-subscriber-types";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:subscriberName",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSubscribers],
|
||||
description: "Get PKI Subscriber",
|
||||
params: z.object({
|
||||
subscriberName: z.string().describe(PKI_SUBSCRIBERS.GET.subscriberName)
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectId: z.string().describe(PKI_SUBSCRIBERS.GET.projectId)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedPkiSubscriber
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const subscriber = await server.services.pkiSubscriber.getSubscriber({
|
||||
subscriberName: req.params.subscriberName,
|
||||
projectId: req.query.projectId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: subscriber.projectId,
|
||||
event: {
|
||||
type: EventType.GET_PKI_SUBSCRIBER,
|
||||
metadata: {
|
||||
pkiSubscriberId: subscriber.id,
|
||||
name: subscriber.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return subscriber;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSubscribers],
|
||||
description: "Create PKI Subscriber",
|
||||
body: z.object({
|
||||
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.CREATE.projectId),
|
||||
caId: z
|
||||
.string()
|
||||
.trim()
|
||||
.uuid("CA ID must be a valid UUID")
|
||||
.min(1, "CA ID is required")
|
||||
.describe(PKI_SUBSCRIBERS.CREATE.caId),
|
||||
name: slugSchema({ min: 1, max: 64, field: "name" }).describe(PKI_SUBSCRIBERS.CREATE.name),
|
||||
commonName: z.string().trim().min(1).describe(PKI_SUBSCRIBERS.CREATE.commonName),
|
||||
status: z
|
||||
.nativeEnum(PkiSubscriberStatus)
|
||||
.default(PkiSubscriberStatus.ACTIVE)
|
||||
.describe(PKI_SUBSCRIBERS.CREATE.status),
|
||||
ttl: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(PKI_SUBSCRIBERS.CREATE.ttl),
|
||||
subjectAlternativeNames: validateAltNameField
|
||||
.array()
|
||||
.default([])
|
||||
.transform((arr) => Array.from(new Set(arr)))
|
||||
.describe(PKI_SUBSCRIBERS.CREATE.subjectAlternativeNames),
|
||||
keyUsages: z
|
||||
.nativeEnum(CertKeyUsage)
|
||||
.array()
|
||||
.default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT])
|
||||
.transform((arr) => Array.from(new Set(arr)))
|
||||
.describe(PKI_SUBSCRIBERS.CREATE.keyUsages),
|
||||
extendedKeyUsages: z
|
||||
.nativeEnum(CertExtendedKeyUsage)
|
||||
.array()
|
||||
.default([])
|
||||
.transform((arr) => Array.from(new Set(arr)))
|
||||
.describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedPkiSubscriber
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const subscriber = await server.services.pkiSubscriber.createSubscriber({
|
||||
...req.body,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: subscriber.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_PKI_SUBSCRIBER,
|
||||
metadata: {
|
||||
pkiSubscriberId: subscriber.id,
|
||||
caId: subscriber.caId ?? undefined,
|
||||
name: subscriber.name,
|
||||
commonName: subscriber.commonName,
|
||||
ttl: subscriber.ttl,
|
||||
subjectAlternativeNames: subscriber.subjectAlternativeNames,
|
||||
keyUsages: subscriber.keyUsages as CertKeyUsage[],
|
||||
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return subscriber;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:subscriberName",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSubscribers],
|
||||
description: "Update PKI Subscriber",
|
||||
params: z.object({
|
||||
subscriberName: z.string().trim().describe(PKI_SUBSCRIBERS.UPDATE.subscriberName)
|
||||
}),
|
||||
body: z.object({
|
||||
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.UPDATE.projectId),
|
||||
caId: z
|
||||
.string()
|
||||
.trim()
|
||||
.uuid("CA ID must be a valid UUID")
|
||||
.min(1, "CA ID is required")
|
||||
.optional()
|
||||
.describe(PKI_SUBSCRIBERS.UPDATE.caId),
|
||||
name: slugSchema({ min: 1, max: 64, field: "name" }).describe(PKI_SUBSCRIBERS.UPDATE.name).optional(),
|
||||
commonName: z.string().trim().min(1).describe(PKI_SUBSCRIBERS.UPDATE.commonName).optional(),
|
||||
status: z.nativeEnum(PkiSubscriberStatus).optional().describe(PKI_SUBSCRIBERS.UPDATE.status),
|
||||
subjectAlternativeNames: validateAltNameField
|
||||
.array()
|
||||
.optional()
|
||||
.describe(PKI_SUBSCRIBERS.UPDATE.subjectAlternativeNames),
|
||||
ttl: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.optional()
|
||||
.describe(PKI_SUBSCRIBERS.UPDATE.ttl),
|
||||
keyUsages: z
|
||||
.nativeEnum(CertKeyUsage)
|
||||
.array()
|
||||
.transform((arr) => Array.from(new Set(arr)))
|
||||
.optional()
|
||||
.describe(PKI_SUBSCRIBERS.UPDATE.keyUsages),
|
||||
extendedKeyUsages: z
|
||||
.nativeEnum(CertExtendedKeyUsage)
|
||||
.array()
|
||||
.transform((arr) => Array.from(new Set(arr)))
|
||||
.optional()
|
||||
.describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedPkiSubscriber
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const subscriber = await server.services.pkiSubscriber.updateSubscriber({
|
||||
subscriberName: req.params.subscriberName,
|
||||
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,
|
||||
projectId: subscriber.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PKI_SUBSCRIBER,
|
||||
metadata: {
|
||||
pkiSubscriberId: subscriber.id,
|
||||
caId: subscriber.caId ?? undefined,
|
||||
name: subscriber.name,
|
||||
commonName: subscriber.commonName,
|
||||
ttl: subscriber.ttl,
|
||||
subjectAlternativeNames: subscriber.subjectAlternativeNames,
|
||||
keyUsages: subscriber.keyUsages as CertKeyUsage[],
|
||||
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return subscriber;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:subscriberName",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSubscribers],
|
||||
description: "Delete PKI Subscriber",
|
||||
params: z.object({
|
||||
subscriberName: z.string().describe(PKI_SUBSCRIBERS.DELETE.subscriberName)
|
||||
}),
|
||||
body: z.object({
|
||||
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.DELETE.projectId)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedPkiSubscriber
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const subscriber = await server.services.pkiSubscriber.deleteSubscriber({
|
||||
subscriberName: req.params.subscriberName,
|
||||
projectId: req.body.projectId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: subscriber.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_PKI_SUBSCRIBER,
|
||||
metadata: {
|
||||
pkiSubscriberId: subscriber.id,
|
||||
name: subscriber.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return subscriber;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:subscriberName/issue-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSubscribers],
|
||||
description: "Issue certificate",
|
||||
params: z.object({
|
||||
subscriberName: z.string().describe(PKI_SUBSCRIBERS.ISSUE_CERT.subscriberName)
|
||||
}),
|
||||
body: z.object({
|
||||
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.certificate),
|
||||
issuingCaCertificate: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.issuingCaCertificate),
|
||||
certificateChain: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.certificateChain),
|
||||
privateKey: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.privateKey),
|
||||
serialNumber: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, privateKey, serialNumber, subscriber } =
|
||||
await server.services.pkiSubscriber.issueSubscriberCert({
|
||||
subscriberName: req.params.subscriberName,
|
||||
projectId: req.body.projectId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: subscriber.projectId,
|
||||
event: {
|
||||
type: EventType.ISSUE_PKI_SUBSCRIBER_CERT,
|
||||
metadata: {
|
||||
subscriberId: subscriber.id,
|
||||
name: subscriber.name,
|
||||
serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
subscriberId: subscriber.id,
|
||||
commonName: subscriber.commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
privateKey,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:subscriberName/sign-certificate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSubscribers],
|
||||
description: "Sign certificate",
|
||||
params: z.object({
|
||||
subscriberName: z.string().describe(PKI_SUBSCRIBERS.SIGN_CERT.subscriberName)
|
||||
}),
|
||||
body: z.object({
|
||||
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.projectId),
|
||||
csr: z.string().trim().min(1).max(3000).describe(PKI_SUBSCRIBERS.SIGN_CERT.csr)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.certificate),
|
||||
issuingCaCertificate: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.issuingCaCertificate),
|
||||
certificateChain: z.string().trim().describe(PKI_SUBSCRIBERS.SIGN_CERT.certificateChain),
|
||||
serialNumber: z.string().trim().describe(PKI_SUBSCRIBERS.ISSUE_CERT.serialNumber)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, subscriber } =
|
||||
await server.services.pkiSubscriber.signSubscriberCert({
|
||||
subscriberName: req.params.subscriberName,
|
||||
projectId: req.body.projectId,
|
||||
csr: req.body.csr,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: subscriber.projectId,
|
||||
event: {
|
||||
type: EventType.SIGN_PKI_SUBSCRIBER_CERT,
|
||||
metadata: {
|
||||
subscriberId: subscriber.id,
|
||||
name: subscriber.name,
|
||||
serialNumber
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SignCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
subscriberId: subscriber.id,
|
||||
commonName: subscriber.commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
serialNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:subscriberName/certificates",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSubscribers],
|
||||
description: "List PKI Subscriber certificates",
|
||||
params: z.object({
|
||||
subscriberName: z.string().describe(PKI_SUBSCRIBERS.GET.subscriberName)
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().describe(PKI_SUBSCRIBERS.LIST_CERTS.projectId),
|
||||
offset: z.coerce.number().min(0).max(100).default(0).describe(PKI_SUBSCRIBERS.LIST_CERTS.offset),
|
||||
limit: z.coerce.number().min(1).max(100).default(25).describe(PKI_SUBSCRIBERS.LIST_CERTS.limit)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificates: z.array(CertificatesSchema),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { totalCount, certificates } = await server.services.pkiSubscriber.listSubscriberCerts({
|
||||
subscriberName: req.params.subscriberName,
|
||||
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.LIST_PKI_SUBSCRIBER_CERTS,
|
||||
metadata: {
|
||||
subscriberId: req.params.subscriberName,
|
||||
name: req.params.subscriberName,
|
||||
projectId: req.query.projectId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificates,
|
||||
totalCount
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
@@ -346,7 +346,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
|
||||
})
|
||||
.optional()
|
||||
.describe(PROJECTS.UPDATE.slug)
|
||||
.describe(PROJECTS.UPDATE.slug),
|
||||
secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -366,7 +367,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
description: req.body.description,
|
||||
autoCapitalization: req.body.autoCapitalization,
|
||||
hasDeleteProtection: req.body.hasDeleteProtection,
|
||||
slug: req.body.slug
|
||||
slug: req.body.slug,
|
||||
secretSharing: req.body.secretSharing
|
||||
},
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
@@ -511,7 +513,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const workspace = await server.services.project.updateAuditLogsRetention({
|
||||
actorId: req.permission.id,
|
||||
|
@@ -62,7 +62,9 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
}),
|
||||
body: z.object({
|
||||
hashedHex: z.string().min(1).optional(),
|
||||
password: z.string().optional()
|
||||
password: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
hash: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -88,7 +90,9 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
sharedSecretId: req.params.id,
|
||||
hashedHex: req.body.hashedHex,
|
||||
password: req.body.password,
|
||||
orgId: req.permission?.orgId
|
||||
orgId: req.permission?.orgId,
|
||||
email: req.body.email,
|
||||
hash: req.body.hash
|
||||
});
|
||||
|
||||
if (sharedSecret.secret?.orgId) {
|
||||
@@ -151,7 +155,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
secretValue: z.string(),
|
||||
expiresAt: z.string(),
|
||||
expiresAfterViews: z.number().min(1).optional(),
|
||||
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
||||
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization),
|
||||
emails: z.string().email().array().max(100).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -10,6 +10,7 @@ import { registerGcpSyncRouter } from "./gcp-sync-router";
|
||||
import { registerGitHubSyncRouter } from "./github-sync-router";
|
||||
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
|
||||
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||
import { registerOCIVaultSyncRouter } from "./oci-vault-sync-router";
|
||||
import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
|
||||
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
|
||||
import { registerVercelSyncRouter } from "./vercel-sync-router";
|
||||
@@ -31,5 +32,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.Vercel]: registerVercelSyncRouter,
|
||||
[SecretSync.Windmill]: registerWindmillSyncRouter,
|
||||
[SecretSync.HCVault]: registerHCVaultSyncRouter,
|
||||
[SecretSync.TeamCity]: registerTeamCitySyncRouter
|
||||
[SecretSync.TeamCity]: registerTeamCitySyncRouter,
|
||||
[SecretSync.OCIVault]: registerOCIVaultSyncRouter
|
||||
};
|
||||
|
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
CreateOCIVaultSyncSchema,
|
||||
OCIVaultSyncSchema,
|
||||
UpdateOCIVaultSyncSchema
|
||||
} from "@app/services/secret-sync/oci-vault";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerOCIVaultSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.OCIVault,
|
||||
server,
|
||||
responseSchema: OCIVaultSyncSchema,
|
||||
createSchema: CreateOCIVaultSyncSchema,
|
||||
updateSchema: UpdateOCIVaultSyncSchema
|
||||
});
|
@@ -24,6 +24,7 @@ import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/
|
||||
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
|
||||
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
|
||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||
import { OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "@app/services/secret-sync/oci-vault";
|
||||
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
|
||||
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
||||
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
||||
@@ -43,7 +44,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
VercelSyncSchema,
|
||||
WindmillSyncSchema,
|
||||
HCVaultSyncSchema,
|
||||
TeamCitySyncSchema
|
||||
TeamCitySyncSchema,
|
||||
OCIVaultSyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
@@ -60,7 +62,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
VercelSyncListItemSchema,
|
||||
WindmillSyncListItemSchema,
|
||||
HCVaultSyncListItemSchema,
|
||||
TeamCitySyncListItemSchema
|
||||
TeamCitySyncListItemSchema,
|
||||
OCIVaultSyncListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||
|
@@ -46,6 +46,54 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/duplicate-accounts",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
users: UsersSchema.extend({
|
||||
isMyAccount: z.boolean(),
|
||||
organizations: z.object({ name: z.string(), slug: z.string() }).array()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
|
||||
handler: async (req) => {
|
||||
if (req.auth.authMode === AuthMode.JWT && req.auth.user.email) {
|
||||
const users = await server.services.user.getAllMyAccounts(req.auth.user.email, req.permission.id);
|
||||
return { users };
|
||||
}
|
||||
return { users: [] };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/remove-duplicate-accounts",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
|
||||
handler: async (req) => {
|
||||
if (req.auth.authMode === AuthMode.JWT && req.auth.user.email) {
|
||||
await server.services.user.removeMyDuplicateAccounts(req.auth.user.email, req.permission.id);
|
||||
}
|
||||
return { message: "Removed all duplicate accounts" };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/private-key",
|
||||
|
@@ -27,8 +27,19 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
||||
projectId: z.string().describe(PROJECT_USERS.INVITE_MEMBER.projectId)
|
||||
}),
|
||||
body: z.object({
|
||||
emails: z.string().email().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.emails),
|
||||
usernames: z.string().array().default([]).describe(PROJECT_USERS.INVITE_MEMBER.usernames),
|
||||
emails: z
|
||||
.string()
|
||||
.email()
|
||||
.array()
|
||||
.default([])
|
||||
.describe(PROJECT_USERS.INVITE_MEMBER.emails)
|
||||
.refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"),
|
||||
usernames: z
|
||||
.string()
|
||||
.array()
|
||||
.default([])
|
||||
.describe(PROJECT_USERS.INVITE_MEMBER.usernames)
|
||||
.refine((val) => val.every((el) => el === el.toLowerCase()), "Username must be lowercase"),
|
||||
roleSlugs: z.string().array().min(1).optional().describe(PROJECT_USERS.INVITE_MEMBER.roleSlugs)
|
||||
}),
|
||||
response: {
|
||||
@@ -92,8 +103,19 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
|
||||
projectId: z.string().describe(PROJECT_USERS.REMOVE_MEMBER.projectId)
|
||||
}),
|
||||
body: z.object({
|
||||
emails: z.string().email().array().default([]).describe(PROJECT_USERS.REMOVE_MEMBER.emails),
|
||||
usernames: z.string().array().default([]).describe(PROJECT_USERS.REMOVE_MEMBER.usernames)
|
||||
emails: z
|
||||
.string()
|
||||
.email()
|
||||
.array()
|
||||
.default([])
|
||||
.describe(PROJECT_USERS.REMOVE_MEMBER.emails)
|
||||
.refine((val) => val.every((el) => el === el.toLowerCase()), "Email must be lowercase"),
|
||||
usernames: z
|
||||
.string()
|
||||
.array()
|
||||
.default([])
|
||||
.describe(PROJECT_USERS.REMOVE_MEMBER.usernames)
|
||||
.refine((val) => val.every((el) => el === el.toLowerCase()), "Username must be lowercase")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user