mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-25 14:07:47 +00:00
Compare commits
177 Commits
remove-mig
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
9b6a315825 | ||
|
13b2f65b7e | ||
|
6cf1e046b0 | ||
|
f6e1441dc0 | ||
|
9eeb72ac80 | ||
|
f6e566a028 | ||
|
a34c74e958 | ||
|
eef7a875a1 | ||
|
09938a911b | ||
|
af08c41008 | ||
|
443c8854ea | ||
|
f7a25e7601 | ||
|
4c6e5c9c4c | ||
|
98a4e6c96d | ||
|
c93ce06409 | ||
|
672e4baec4 | ||
|
b5ef2a6837 | ||
|
9c611daada | ||
|
71edb08942 | ||
|
89d8261a43 | ||
|
a2b2b07185 | ||
|
76864ababa | ||
|
52858dad79 | ||
|
1d7a6ea50e | ||
|
c031233247 | ||
|
d17d40ebd9 | ||
|
70fff1f2da | ||
|
3f8eaa0679 | ||
|
50d0035d7b | ||
|
9743ad02d5 | ||
|
50f5248e3e | ||
|
8d7b573988 | ||
|
26d0ab1dc2 | ||
|
4acdbd24e9 | ||
|
c3c907788a | ||
|
bf833a57cd | ||
|
e8519f6612 | ||
|
0b4675e7b5 | ||
|
07df6803a5 | ||
|
a09d0e8948 | ||
|
ee598560ec | ||
|
2793ac22aa | ||
|
31fad03af8 | ||
|
c629705c9c | ||
|
be10f6e52a | ||
|
40c5ff0ad6 | ||
|
8ecb5ca7bc | ||
|
ab6a2b7dbb | ||
|
81bfc04e7c | ||
|
a757fceaed | ||
|
ce8e18f620 | ||
|
d09c964647 | ||
|
eeddbde600 | ||
|
859b643e43 | ||
|
91f71e0ef6 | ||
|
4e9e31eeb7 | ||
|
f6bc99b964 | ||
|
679eb9dffc | ||
|
0754ae3aaf | ||
|
519a0c1bdf | ||
|
e9d8979cf4 | ||
|
486d975fa0 | ||
|
42c49949b4 | ||
|
aea44088db | ||
|
cd71db416d | ||
|
9d682ca874 | ||
|
9054db80ad | ||
|
5bb8756c67 | ||
|
8b7cb4c4eb | ||
|
e584c9ea95 | ||
|
428c60880a | ||
|
2179b9a4d7 | ||
|
1921763fa8 | ||
|
5408859a18 | ||
|
8dfc0cfbe0 | ||
|
060199e58c | ||
|
3b9b17f8d5 | ||
|
6addde2650 | ||
|
5b7627585f | ||
|
800ea5ce78 | ||
|
a6b3be72a9 | ||
|
394bd6755f | ||
|
c21873ac4b | ||
|
64b8c1a2de | ||
|
de443c5ea1 | ||
|
a3b7df4e6b | ||
|
531607dcb7 | ||
|
182de009b2 | ||
|
f1651ce171 | ||
|
e1f563dbd4 | ||
|
107cca0b62 | ||
|
72abc08f04 | ||
|
a4b648ad95 | ||
|
04a8931cf6 | ||
|
ab0b8c0f10 | ||
|
258836a605 | ||
|
d6b31cde44 | ||
|
2c94f9ec3c | ||
|
42ad63b58d | ||
|
f2d5112585 | ||
|
9c7b25de49 | ||
|
0b31d7f860 | ||
|
5c91d380b8 | ||
|
b908893a68 | ||
|
4d0275e589 | ||
|
befd77eec2 | ||
|
1d44774913 | ||
|
984552eea9 | ||
|
b6a957a30d | ||
|
36954a9df9 | ||
|
2f4efad8ae | ||
|
16c476d78c | ||
|
68c549f1c6 | ||
|
0610416677 | ||
|
4a37dc9cb7 | ||
|
7e432a4297 | ||
|
794fc9c2a2 | ||
|
d4e5d2c7ed | ||
|
581840a701 | ||
|
0c2e0bb0f9 | ||
|
e2a414ffff | ||
|
0ca3c2bb68 | ||
|
083581b51a | ||
|
40e976133c | ||
|
ad2f002822 | ||
|
8842dfe5d1 | ||
|
326742c2d5 | ||
|
b1eea4ae9c | ||
|
a8e0a8aca3 | ||
|
b37058d0e2 | ||
|
c891b8f5d3 | ||
|
a32bb95703 | ||
|
334a05d5f1 | ||
|
12c813928c | ||
|
521fef6fca | ||
|
8f8236c445 | ||
|
3cf5c534ff | ||
|
2b03c295f9 | ||
|
4fc7a52941 | ||
|
0ded2e51ba | ||
|
0d2b3adec7 | ||
|
e695203c05 | ||
|
f9d76aae5d | ||
|
1c280759d1 | ||
|
4562f57b54 | ||
|
6005dce44d | ||
|
0410c83cef | ||
|
cf4f2ea6b1 | ||
|
bf85df7e36 | ||
|
f7f7d2d528 | ||
|
57342cf2a0 | ||
|
86bb2659b5 | ||
|
dc59f226b6 | ||
|
9175c1dffa | ||
|
b9070a8fa3 | ||
|
1e4dfd0c7c | ||
|
34b7d28e2f | ||
|
245a348517 | ||
|
e0fc582e2e | ||
|
68ef897b6a | ||
|
1b060e76de | ||
|
9f7599b2a1 | ||
|
9cbe70a6f3 | ||
|
f49fb534ab | ||
|
6eea4c8364 | ||
|
1e206ee441 | ||
|
85c1a1081e | ||
|
877485b45a | ||
|
d13e685a81 | ||
|
9849a5f136 | ||
|
26773a1444 | ||
|
3ea450e94a | ||
|
7d0574087c | ||
|
36916704be | ||
|
a6f280197b | ||
|
346d2f213e | ||
|
9f1ac77afa |
@@ -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
|
||||
@@ -171,6 +171,7 @@ ENV NODE_ENV production
|
||||
ENV STANDALONE_BUILD true
|
||||
ENV STANDALONE_MODE true
|
||||
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
|
@@ -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 /
|
||||
@@ -168,6 +168,7 @@ ENV HTTPS_ENABLED false
|
||||
ENV NODE_ENV production
|
||||
ENV STANDALONE_BUILD true
|
||||
ENV STANDALONE_MODE true
|
||||
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import RE2 from "re2";
|
||||
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { applyJitter } from "@app/lib/dates";
|
||||
import { delay as delayMs } from "@app/lib/delay";
|
||||
import { Lock } from "@app/lib/red-lock";
|
||||
|
||||
export const mockKeyStore = (): TKeyStoreFactory => {
|
||||
@@ -18,6 +22,27 @@ export const mockKeyStore = (): TKeyStoreFactory => {
|
||||
delete store[key];
|
||||
return 1;
|
||||
},
|
||||
deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => {
|
||||
const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
||||
let totalDeleted = 0;
|
||||
const keys = Object.keys(store);
|
||||
|
||||
for (let i = 0; i < keys.length; i += batchSize) {
|
||||
const batch = keys.slice(i, i + batchSize);
|
||||
|
||||
for (const key of batch) {
|
||||
if (regex.test(key)) {
|
||||
delete store[key];
|
||||
totalDeleted += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delayMs(Math.max(0, applyJitter(delay, jitter)));
|
||||
}
|
||||
|
||||
return totalDeleted;
|
||||
},
|
||||
getItem: async (key) => {
|
||||
const value = store[key];
|
||||
if (typeof value === "string") {
|
||||
|
2303
backend/package-lock.json
generated
2303
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",
|
||||
@@ -152,7 +152,8 @@
|
||||
"@infisical/quic": "^1.0.8",
|
||||
"@node-saml/passport-saml": "^5.0.1",
|
||||
"@octokit/auth-app": "^7.1.1",
|
||||
"@octokit/plugin-paginate-graphql": "^5.2.4",
|
||||
"@octokit/core": "^5.2.1",
|
||||
"@octokit/plugin-paginate-graphql": "^4.0.1",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
@@ -208,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",
|
||||
@@ -240,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"
|
||||
}
|
||||
}
|
||||
|
16
backend/src/@types/fastify.d.ts
vendored
16
backend/src/@types/fastify.d.ts
vendored
@@ -66,6 +66,8 @@ import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-a
|
||||
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
|
||||
import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service";
|
||||
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 { 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";
|
||||
@@ -78,6 +80,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";
|
||||
@@ -146,6 +149,13 @@ declare module "fastify" {
|
||||
providerAuthToken: string;
|
||||
externalProviderAccessToken?: string;
|
||||
};
|
||||
passportMachineIdentity: {
|
||||
identityId: string;
|
||||
user: {
|
||||
uid: string;
|
||||
mail?: string;
|
||||
};
|
||||
};
|
||||
kmipUser: {
|
||||
projectId: string;
|
||||
clientId: string;
|
||||
@@ -153,7 +163,9 @@ declare module "fastify" {
|
||||
};
|
||||
auditLogInfo: Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
|
||||
ssoConfig: Awaited<ReturnType<TSamlConfigServiceFactory["getSaml"]>>;
|
||||
ldapConfig: Awaited<ReturnType<TLdapConfigServiceFactory["getLdapCfg"]>>;
|
||||
ldapConfig: Awaited<ReturnType<TLdapConfigServiceFactory["getLdapCfg"]>> & {
|
||||
allowedFields?: TAllowedFields[];
|
||||
};
|
||||
}
|
||||
|
||||
interface FastifyInstance {
|
||||
@@ -199,6 +211,7 @@ declare module "fastify" {
|
||||
identityAzureAuth: TIdentityAzureAuthServiceFactory;
|
||||
identityOidcAuth: TIdentityOidcAuthServiceFactory;
|
||||
identityJwtAuth: TIdentityJwtAuthServiceFactory;
|
||||
identityLdapAuth: TIdentityLdapAuthServiceFactory;
|
||||
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
|
||||
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
|
||||
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
||||
@@ -220,6 +233,7 @@ declare module "fastify" {
|
||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||
certificateEst: TCertificateEstServiceFactory;
|
||||
pkiCollection: TPkiCollectionServiceFactory;
|
||||
pkiSubscriber: TPkiSubscriberServiceFactory;
|
||||
secretScanning: TSecretScanningServiceFactory;
|
||||
license: TLicenseServiceFactory;
|
||||
trustedIp: TTrustedIpServiceFactory;
|
||||
|
18
backend/src/@types/knex.d.ts
vendored
18
backend/src/@types/knex.d.ts
vendored
@@ -209,6 +209,9 @@ import {
|
||||
TPkiCollections,
|
||||
TPkiCollectionsInsert,
|
||||
TPkiCollectionsUpdate,
|
||||
TPkiSubscribers,
|
||||
TPkiSubscribersInsert,
|
||||
TPkiSubscribersUpdate,
|
||||
TProjectBots,
|
||||
TProjectBotsInsert,
|
||||
TProjectBotsUpdate,
|
||||
@@ -432,6 +435,11 @@ import {
|
||||
TWorkflowIntegrationsInsert,
|
||||
TWorkflowIntegrationsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import {
|
||||
TIdentityLdapAuths,
|
||||
TIdentityLdapAuthsInsert,
|
||||
TIdentityLdapAuthsUpdate
|
||||
} from "@app/db/schemas/identity-ldap-auths";
|
||||
import {
|
||||
TMicrosoftTeamsIntegrations,
|
||||
TMicrosoftTeamsIntegrationsInsert,
|
||||
@@ -559,6 +567,11 @@ declare module "knex/types/tables" {
|
||||
TPkiCollectionItemsInsert,
|
||||
TPkiCollectionItemsUpdate
|
||||
>;
|
||||
[TableName.PkiSubscriber]: KnexOriginal.CompositeTableType<
|
||||
TPkiSubscribers,
|
||||
TPkiSubscribersInsert,
|
||||
TPkiSubscribersUpdate
|
||||
>;
|
||||
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
||||
TUserGroupMembership,
|
||||
TUserGroupMembershipInsert,
|
||||
@@ -735,6 +748,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityJwtAuthsInsert,
|
||||
TIdentityJwtAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityLdapAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityLdapAuths,
|
||||
TIdentityLdapAuthsInsert,
|
||||
TIdentityLdapAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType<
|
||||
TIdentityUaClientSecrets,
|
||||
TIdentityUaClientSecretsInsert,
|
||||
|
@@ -3,7 +3,7 @@ import { Knex } from "knex";
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.CertificateBody)) {
|
||||
if (!(await knex.schema.hasColumn(TableName.CertificateBody, "encryptedCertificateChain"))) {
|
||||
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
|
||||
t.binary("encryptedCertificateChain").nullable();
|
||||
});
|
||||
@@ -25,7 +25,7 @@ export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTable(TableName.CertificateSecret);
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.CertificateBody)) {
|
||||
if (await knex.schema.hasColumn(TableName.CertificateBody, "encryptedCertificateChain")) {
|
||||
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
|
||||
t.dropColumn("encryptedCertificateChain");
|
||||
});
|
||||
|
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ProjectType, TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.ProjectTemplates, "type"))) {
|
||||
await knex.schema.alterTable(TableName.ProjectTemplates, (t) => {
|
||||
// defaulting to sm for migration to set existing, new ones will always be specified on creation
|
||||
t.string("type").defaultTo(ProjectType.SecretManager).notNullable();
|
||||
t.jsonb("environments").nullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.ProjectTemplates, "type")) {
|
||||
await knex.schema.alterTable(TableName.ProjectTemplates, (t) => {
|
||||
t.dropColumn("type");
|
||||
// not reverting nullable environments
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
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.IdentityLdapAuth))) {
|
||||
await knex.schema.createTable(TableName.IdentityLdapAuth, (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.uuid("identityId").notNullable().unique();
|
||||
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
|
||||
t.binary("encryptedBindDN").notNullable();
|
||||
t.binary("encryptedBindPass").notNullable();
|
||||
t.binary("encryptedLdapCaCertificate").nullable();
|
||||
|
||||
t.string("url").notNullable();
|
||||
t.string("searchBase").notNullable();
|
||||
t.string("searchFilter").notNullable();
|
||||
|
||||
t.jsonb("allowedFields").nullable();
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.IdentityLdapAuth);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityLdapAuth);
|
||||
await dropOnUpdateTrigger(knex, TableName.IdentityLdapAuth);
|
||||
}
|
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);
|
||||
}
|
@@ -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>;
|
||||
|
32
backend/src/db/schemas/identity-ldap-auths.ts
Normal file
32
backend/src/db/schemas/identity-ldap-auths.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const IdentityLdapAuthsSchema = 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(),
|
||||
identityId: z.string().uuid(),
|
||||
encryptedBindDN: zodBuffer,
|
||||
encryptedBindPass: zodBuffer,
|
||||
encryptedLdapCaCertificate: zodBuffer.nullable().optional(),
|
||||
url: z.string(),
|
||||
searchBase: z.string(),
|
||||
searchFilter: z.string(),
|
||||
allowedFields: z.unknown().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TIdentityLdapAuths = z.infer<typeof IdentityLdapAuthsSchema>;
|
||||
export type TIdentityLdapAuthsInsert = Omit<z.input<typeof IdentityLdapAuthsSchema>, TImmutableDBKeys>;
|
||||
export type TIdentityLdapAuthsUpdate = Partial<Omit<z.input<typeof IdentityLdapAuthsSchema>, TImmutableDBKeys>>;
|
@@ -69,6 +69,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",
|
||||
@@ -80,6 +81,7 @@ export enum TableName {
|
||||
IdentityAwsAuth = "identity_aws_auths",
|
||||
IdentityOidcAuth = "identity_oidc_auths",
|
||||
IdentityJwtAuth = "identity_jwt_auths",
|
||||
IdentityLdapAuth = "identity_ldap_auths",
|
||||
IdentityOrgMembership = "identity_org_memberships",
|
||||
IdentityProjectMembership = "identity_project_memberships",
|
||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||
@@ -185,11 +187,16 @@ export enum OrgMembershipStatus {
|
||||
}
|
||||
|
||||
export enum ProjectMembershipRole {
|
||||
// general
|
||||
Admin = "admin",
|
||||
Member = "member",
|
||||
Custom = "custom",
|
||||
Viewer = "viewer",
|
||||
NoAccess = "no-access"
|
||||
NoAccess = "no-access",
|
||||
// ssh
|
||||
SshHostBootstrapper = "ssh-host-bootstrapper",
|
||||
// kms
|
||||
KmsCryptographicOperator = "cryptographic-operator"
|
||||
}
|
||||
|
||||
export enum SecretEncryptionAlgo {
|
||||
@@ -227,7 +234,8 @@ export enum IdentityAuthMethod {
|
||||
AWS_AUTH = "aws-auth",
|
||||
AZURE_AUTH = "azure-auth",
|
||||
OIDC_AUTH = "oidc-auth",
|
||||
JWT_AUTH = "jwt-auth"
|
||||
JWT_AUTH = "jwt-auth",
|
||||
LDAP_AUTH = "ldap-auth"
|
||||
}
|
||||
|
||||
export enum ProjectType {
|
||||
|
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>>;
|
@@ -12,10 +12,11 @@ export const ProjectTemplatesSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
roles: z.unknown(),
|
||||
environments: z.unknown(),
|
||||
environments: z.unknown().nullable().optional(),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
type: z.string().default("secret-manager")
|
||||
});
|
||||
|
||||
export type TProjectTemplates = z.infer<typeof ProjectTemplatesSchema>;
|
||||
|
@@ -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>;
|
||||
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@@ -18,6 +19,9 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
server.route({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
permissions: z.any().array(),
|
||||
|
@@ -98,6 +98,9 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/login",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
organizationSlug: z.string().trim()
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
|
||||
import { ProjectMembershipRole, ProjectTemplatesSchema, ProjectType } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
|
||||
import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
|
||||
import { ApiDocsTags, ProjectTemplates } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@@ -35,6 +34,7 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
|
||||
position: z.number().min(1)
|
||||
})
|
||||
.array()
|
||||
.nullable()
|
||||
});
|
||||
|
||||
const ProjectTemplateRolesSchema = z
|
||||
@@ -104,6 +104,9 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.ProjectTemplates],
|
||||
description: "List project templates for the current organization.",
|
||||
querystring: z.object({
|
||||
type: z.nativeEnum(ProjectType).optional().describe(ProjectTemplates.LIST.type)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
projectTemplates: SanitizedProjectTemplateSchema.array()
|
||||
@@ -112,7 +115,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission);
|
||||
const { type } = req.query;
|
||||
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission, type);
|
||||
|
||||
const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name));
|
||||
|
||||
@@ -184,6 +188,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
||||
tags: [ApiDocsTags.ProjectTemplates],
|
||||
description: "Create a project template.",
|
||||
body: z.object({
|
||||
type: z.nativeEnum(ProjectType).describe(ProjectTemplates.CREATE.type),
|
||||
name: slugSchema({ field: "name" })
|
||||
.refine((val) => !isInfisicalProjectTemplate(val), {
|
||||
message: `The requested project template name is reserved.`
|
||||
@@ -191,9 +196,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
||||
.describe(ProjectTemplates.CREATE.name),
|
||||
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
|
||||
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
|
||||
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
|
||||
ProjectTemplates.CREATE.environments
|
||||
)
|
||||
environments: ProjectTemplateEnvironmentsSchema.describe(ProjectTemplates.CREATE.environments).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -166,6 +166,9 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/redirect/saml2/organizations/:orgSlug",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
orgSlug: z.string().trim()
|
||||
@@ -192,6 +195,9 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/redirect/saml2/:samlConfigId",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
samlConfigId: z.string().trim()
|
||||
@@ -218,6 +224,9 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/saml2/:samlConfigId",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
samlConfigId: z.string().trim()
|
||||
|
@@ -196,6 +196,9 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
url: "/Users",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
schemas: z.array(z.string()),
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
|
||||
import { canUseSecretScanning } from "@app/ee/services/secret-scanning/secret-scanning-fns";
|
||||
import {
|
||||
SecretScanningResolvedStatus,
|
||||
SecretScanningRiskStatus
|
||||
} from "@app/ee/services/secret-scanning/secret-scanning-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@@ -23,14 +23,14 @@ export const registerSecretScanningRouter = async (server: FastifyZodProvider) =
|
||||
body: z.object({ organizationId: z.string().trim() }),
|
||||
response: {
|
||||
200: z.object({
|
||||
sessionId: z.string()
|
||||
sessionId: z.string(),
|
||||
gitAppSlug: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(req.auth.orgId)) {
|
||||
if (!canUseSecretScanning(req.auth.orgId)) {
|
||||
throw new BadRequestError({
|
||||
message: "Secret scanning is temporarily unavailable."
|
||||
});
|
||||
|
@@ -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,9 +19,10 @@ 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";
|
||||
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||
import { SecretSync, SecretSyncImportBehavior } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
@@ -119,44 +120,60 @@ export enum EventType {
|
||||
CREATE_TOKEN_IDENTITY_TOKEN_AUTH = "create-token-identity-token-auth",
|
||||
UPDATE_TOKEN_IDENTITY_TOKEN_AUTH = "update-token-identity-token-auth",
|
||||
GET_TOKENS_IDENTITY_TOKEN_AUTH = "get-tokens-identity-token-auth",
|
||||
|
||||
ADD_IDENTITY_TOKEN_AUTH = "add-identity-token-auth",
|
||||
UPDATE_IDENTITY_TOKEN_AUTH = "update-identity-token-auth",
|
||||
GET_IDENTITY_TOKEN_AUTH = "get-identity-token-auth",
|
||||
REVOKE_IDENTITY_TOKEN_AUTH = "revoke-identity-token-auth",
|
||||
|
||||
LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth",
|
||||
ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth",
|
||||
UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth",
|
||||
GET_IDENTITY_KUBERNETES_AUTH = "get-identity-kubernetes-auth",
|
||||
REVOKE_IDENTITY_KUBERNETES_AUTH = "revoke-identity-kubernetes-auth",
|
||||
|
||||
LOGIN_IDENTITY_OIDC_AUTH = "login-identity-oidc-auth",
|
||||
ADD_IDENTITY_OIDC_AUTH = "add-identity-oidc-auth",
|
||||
UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth",
|
||||
GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth",
|
||||
REVOKE_IDENTITY_OIDC_AUTH = "revoke-identity-oidc-auth",
|
||||
|
||||
LOGIN_IDENTITY_JWT_AUTH = "login-identity-jwt-auth",
|
||||
ADD_IDENTITY_JWT_AUTH = "add-identity-jwt-auth",
|
||||
UPDATE_IDENTITY_JWT_AUTH = "update-identity-jwt-auth",
|
||||
GET_IDENTITY_JWT_AUTH = "get-identity-jwt-auth",
|
||||
REVOKE_IDENTITY_JWT_AUTH = "revoke-identity-jwt-auth",
|
||||
|
||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
||||
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
|
||||
|
||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
|
||||
|
||||
LOGIN_IDENTITY_GCP_AUTH = "login-identity-gcp-auth",
|
||||
ADD_IDENTITY_GCP_AUTH = "add-identity-gcp-auth",
|
||||
UPDATE_IDENTITY_GCP_AUTH = "update-identity-gcp-auth",
|
||||
REVOKE_IDENTITY_GCP_AUTH = "revoke-identity-gcp-auth",
|
||||
GET_IDENTITY_GCP_AUTH = "get-identity-gcp-auth",
|
||||
|
||||
LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth",
|
||||
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
|
||||
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
|
||||
REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth",
|
||||
GET_IDENTITY_AWS_AUTH = "get-identity-aws-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",
|
||||
GET_IDENTITY_AZURE_AUTH = "get-identity-azure-auth",
|
||||
REVOKE_IDENTITY_AZURE_AUTH = "revoke-identity-azure-auth",
|
||||
|
||||
LOGIN_IDENTITY_LDAP_AUTH = "login-identity-ldap-auth",
|
||||
ADD_IDENTITY_LDAP_AUTH = "add-identity-ldap-auth",
|
||||
UPDATE_IDENTITY_LDAP_AUTH = "update-identity-ldap-auth",
|
||||
GET_IDENTITY_LDAP_AUTH = "get-identity-ldap-auth",
|
||||
REVOKE_IDENTITY_LDAP_AUTH = "revoke-identity-ldap-auth",
|
||||
|
||||
CREATE_ENVIRONMENT = "create-environment",
|
||||
UPDATE_ENVIRONMENT = "update-environment",
|
||||
DELETE_ENVIRONMENT = "delete-environment",
|
||||
@@ -237,6 +254,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",
|
||||
@@ -1034,6 +1058,55 @@ interface GetIdentityAzureAuthEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityLdapAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_LDAP_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
ldapUsername: string;
|
||||
ldapEmail?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddIdentityLdapAuthEvent {
|
||||
type: EventType.ADD_IDENTITY_LDAP_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
allowedFields?: TAllowedFields[];
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateIdentityLdapAuthEvent {
|
||||
type: EventType.UPDATE_IDENTITY_LDAP_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
allowedFields?: TAllowedFields[];
|
||||
url?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetIdentityLdapAuthEvent {
|
||||
type: EventType.GET_IDENTITY_LDAP_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RevokeIdentityLdapAuthEvent {
|
||||
type: EventType.REVOKE_IDENTITY_LDAP_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityOidcAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_OIDC_AUTH;
|
||||
metadata: {
|
||||
@@ -1899,6 +1972,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: {
|
||||
@@ -2785,6 +2929,11 @@ export type Event =
|
||||
| UpdateIdentityJwtAuthEvent
|
||||
| GetIdentityJwtAuthEvent
|
||||
| DeleteIdentityJwtAuthEvent
|
||||
| LoginIdentityLdapAuthEvent
|
||||
| AddIdentityLdapAuthEvent
|
||||
| UpdateIdentityLdapAuthEvent
|
||||
| GetIdentityLdapAuthEvent
|
||||
| RevokeIdentityLdapAuthEvent
|
||||
| CreateEnvironmentEvent
|
||||
| GetEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
@@ -2857,6 +3006,13 @@ export type Event =
|
||||
| GetPkiCollectionItems
|
||||
| AddPkiCollectionItem
|
||||
| DeletePkiCollectionItem
|
||||
| CreatePkiSubscriber
|
||||
| UpdatePkiSubscriber
|
||||
| DeletePkiSubscriber
|
||||
| GetPkiSubscriber
|
||||
| IssuePkiSubscriberCert
|
||||
| SignPkiSubscriberCert
|
||||
| ListPkiSubscriberCerts
|
||||
| CreateKmsEvent
|
||||
| UpdateKmsEvent
|
||||
| DeleteKmsEvent
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Octokit } from "@octokit/core";
|
||||
import { paginateGraphQL } from "@octokit/plugin-paginate-graphql";
|
||||
import { paginateGraphql } from "@octokit/plugin-paginate-graphql";
|
||||
import { Octokit as OctokitRest } from "@octokit/rest";
|
||||
|
||||
import { OrgMembershipRole } from "@app/db/schemas";
|
||||
@@ -18,7 +18,7 @@ import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TGithubOrgSyncDALFactory } from "./github-org-sync-dal";
|
||||
import { TCreateGithubOrgSyncDTO, TDeleteGithubOrgSyncDTO, TUpdateGithubOrgSyncDTO } from "./github-org-sync-types";
|
||||
|
||||
const OctokitWithPlugin = Octokit.plugin(paginateGraphQL);
|
||||
const OctokitWithPlugin = Octokit.plugin(paginateGraphql);
|
||||
|
||||
type TGithubOrgSyncServiceFactoryDep = {
|
||||
githubOrgSyncDAL: TGithubOrgSyncDALFactory;
|
||||
|
@@ -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
|
||||
};
|
||||
};
|
||||
|
@@ -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;
|
||||
|
@@ -14,6 +14,11 @@ export type TLDAPConfig = {
|
||||
caCert: string;
|
||||
};
|
||||
|
||||
export type TTestLDAPConfigDTO = Omit<
|
||||
TLDAPConfig,
|
||||
"organization" | "id" | "groupSearchBase" | "groupSearchFilter" | "isActive" | "uniqueUserAttribute" | "searchBase"
|
||||
>;
|
||||
|
||||
export type TCreateLdapCfgDTO = {
|
||||
orgId: string;
|
||||
isActive: boolean;
|
||||
|
@@ -2,15 +2,14 @@ import ldapjs from "ldapjs";
|
||||
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TLDAPConfig } from "./ldap-config-types";
|
||||
import { TLDAPConfig, TTestLDAPConfigDTO } from "./ldap-config-types";
|
||||
|
||||
export const isValidLdapFilter = (filter: string) => {
|
||||
try {
|
||||
ldapjs.parseFilter(filter);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("Invalid LDAP filter");
|
||||
logger.error(error);
|
||||
logger.error(error, "Invalid LDAP filter");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -20,7 +19,7 @@ export const isValidLdapFilter = (filter: string) => {
|
||||
* @param ldapConfig - The LDAP configuration to test
|
||||
* @returns {Boolean} isConnected - Whether or not the connection was successful
|
||||
*/
|
||||
export const testLDAPConfig = async (ldapConfig: TLDAPConfig): Promise<boolean> => {
|
||||
export const testLDAPConfig = async (ldapConfig: TTestLDAPConfigDTO): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const ldapClient = ldapjs.createClient({
|
||||
url: ldapConfig.url,
|
||||
|
462
backend/src/ee/services/permission/default-roles.ts
Normal file
462
backend/src/ee/services/permission/default-roles.ts
Normal file
@@ -0,0 +1,462 @@
|
||||
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability";
|
||||
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionCmekActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionGroupActions,
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSecretSyncActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSshHostActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
|
||||
const buildAdminPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
// Admins get full access to everything
|
||||
[
|
||||
ProjectPermissionSub.SecretFolders,
|
||||
ProjectPermissionSub.SecretImports,
|
||||
ProjectPermissionSub.SecretApproval,
|
||||
ProjectPermissionSub.Role,
|
||||
ProjectPermissionSub.Integrations,
|
||||
ProjectPermissionSub.Webhooks,
|
||||
ProjectPermissionSub.ServiceTokens,
|
||||
ProjectPermissionSub.Settings,
|
||||
ProjectPermissionSub.Environments,
|
||||
ProjectPermissionSub.Tags,
|
||||
ProjectPermissionSub.AuditLogs,
|
||||
ProjectPermissionSub.IpAllowList,
|
||||
ProjectPermissionSub.CertificateAuthorities,
|
||||
ProjectPermissionSub.CertificateTemplates,
|
||||
ProjectPermissionSub.PkiAlerts,
|
||||
ProjectPermissionSub.PkiCollections,
|
||||
ProjectPermissionSub.SshCertificateAuthorities,
|
||||
ProjectPermissionSub.SshCertificates,
|
||||
ProjectPermissionSub.SshCertificateTemplates,
|
||||
ProjectPermissionSub.SshHostGroups
|
||||
].forEach((el) => {
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
el
|
||||
);
|
||||
});
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionCertificateActions.Edit,
|
||||
ProjectPermissionCertificateActions.Create,
|
||||
ProjectPermissionCertificateActions.Delete,
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey
|
||||
],
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSshHostActions.Edit,
|
||||
ProjectPermissionSshHostActions.Read,
|
||||
ProjectPermissionSshHostActions.Create,
|
||||
ProjectPermissionSshHostActions.Delete,
|
||||
ProjectPermissionSshHostActions.IssueHostCert
|
||||
],
|
||||
ProjectPermissionSub.SshHosts
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionPkiSubscriberActions.Edit,
|
||||
ProjectPermissionPkiSubscriberActions.Read,
|
||||
ProjectPermissionPkiSubscriberActions.Create,
|
||||
ProjectPermissionPkiSubscriberActions.Delete,
|
||||
ProjectPermissionPkiSubscriberActions.IssueCert,
|
||||
ProjectPermissionPkiSubscriberActions.ListCerts
|
||||
],
|
||||
ProjectPermissionSub.PkiSubscribers
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionMemberActions.Create,
|
||||
ProjectPermissionMemberActions.Edit,
|
||||
ProjectPermissionMemberActions.Delete,
|
||||
ProjectPermissionMemberActions.Read,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionMemberActions.AssumePrivileges
|
||||
],
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionGroupActions.Create,
|
||||
ProjectPermissionGroupActions.Edit,
|
||||
ProjectPermissionGroupActions.Delete,
|
||||
ProjectPermissionGroupActions.Read,
|
||||
ProjectPermissionGroupActions.GrantPrivileges
|
||||
],
|
||||
ProjectPermissionSub.Groups
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionIdentityActions.Create,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionIdentityActions.Delete,
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionIdentityActions.AssumePrivileges
|
||||
],
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
ProjectPermissionSecretActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.Lease
|
||||
],
|
||||
ProjectPermissionSub.DynamicSecrets
|
||||
);
|
||||
|
||||
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCmekActions.Create,
|
||||
ProjectPermissionCmekActions.Edit,
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt,
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionCmekActions.Verify
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretSyncActions.Create,
|
||||
ProjectPermissionSecretSyncActions.Edit,
|
||||
ProjectPermissionSecretSyncActions.Delete,
|
||||
ProjectPermissionSecretSyncActions.Read,
|
||||
ProjectPermissionSecretSyncActions.SyncSecrets,
|
||||
ProjectPermissionSecretSyncActions.ImportSecrets,
|
||||
ProjectPermissionSecretSyncActions.RemoveSecrets
|
||||
],
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionKmipActions.CreateClients,
|
||||
ProjectPermissionKmipActions.UpdateClients,
|
||||
ProjectPermissionKmipActions.DeleteClients,
|
||||
ProjectPermissionKmipActions.ReadClients,
|
||||
ProjectPermissionKmipActions.GenerateClientCertificates
|
||||
],
|
||||
ProjectPermissionSub.Kmip
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretRotationActions.Create,
|
||||
ProjectPermissionSecretRotationActions.Edit,
|
||||
ProjectPermissionSecretRotationActions.Delete,
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
ProjectPermissionSecretRotationActions.ReadGeneratedCredentials,
|
||||
ProjectPermissionSecretRotationActions.RotateSecrets
|
||||
],
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
const buildMemberPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
ProjectPermissionSecretActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.SecretFolders
|
||||
);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.Lease
|
||||
],
|
||||
ProjectPermissionSub.DynamicSecrets
|
||||
);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.SecretImports
|
||||
);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
||||
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
|
||||
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||
|
||||
can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member);
|
||||
|
||||
can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionIdentityActions.Create,
|
||||
ProjectPermissionIdentityActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Settings
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Environments
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Tags
|
||||
);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.Role);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
|
||||
|
||||
// double check if all CRUD are needed for CA and Certificates
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionCertificateActions.Edit,
|
||||
ProjectPermissionCertificateActions.Create,
|
||||
ProjectPermissionCertificateActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
|
||||
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||
|
||||
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
|
||||
can([ProjectPermissionPkiSubscriberActions.Read], ProjectPermissionSub.PkiSubscribers);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCmekActions.Create,
|
||||
ProjectPermissionCmekActions.Edit,
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt,
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionCmekActions.Verify
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretSyncActions.Create,
|
||||
ProjectPermissionSecretSyncActions.Edit,
|
||||
ProjectPermissionSecretSyncActions.Delete,
|
||||
ProjectPermissionSecretSyncActions.Read,
|
||||
ProjectPermissionSecretSyncActions.SyncSecrets,
|
||||
ProjectPermissionSecretSyncActions.ImportSecrets,
|
||||
ProjectPermissionSecretSyncActions.RemoveSecrets
|
||||
],
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
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(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
|
||||
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
can(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
const buildNoAccessProjectPermission = () => {
|
||||
const { rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
return rules;
|
||||
};
|
||||
|
||||
const buildSshHostBootstrapPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(
|
||||
[ProjectPermissionSshHostActions.Create, ProjectPermissionSshHostActions.IssueHostCert],
|
||||
ProjectPermissionSub.SshHosts
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
const buildCryptographicOperatorPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt,
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionCmekActions.Verify
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
// General
|
||||
export const projectAdminPermissions = buildAdminPermissionRules();
|
||||
export const projectMemberPermissions = buildMemberPermissionRules();
|
||||
export const projectViewerPermission = buildViewerPermissionRules();
|
||||
export const projectNoAccessPermissions = buildNoAccessProjectPermission();
|
||||
|
||||
// SSH
|
||||
export const sshHostBootstrapPermissions = buildSshHostBootstrapPermissionRules();
|
||||
|
||||
// KMS
|
||||
export const cryptographicOperatorPermissions = buildCryptographicOperatorPermissionRules();
|
@@ -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"),
|
||||
|
@@ -12,6 +12,14 @@ import {
|
||||
TIdentityProjectMemberships,
|
||||
TProjectMemberships
|
||||
} from "@app/db/schemas";
|
||||
import {
|
||||
cryptographicOperatorPermissions,
|
||||
projectAdminPermissions,
|
||||
projectMemberPermissions,
|
||||
projectNoAccessPermissions,
|
||||
projectViewerPermission,
|
||||
sshHostBootstrapPermissions
|
||||
} from "@app/ee/services/permission/default-roles";
|
||||
import { conditionsMatcher } from "@app/lib/casl";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { objectify } from "@app/lib/fn";
|
||||
@@ -32,14 +40,7 @@ import {
|
||||
TGetServiceTokenProjectPermissionArg,
|
||||
TGetUserProjectPermissionArg
|
||||
} from "./permission-service-types";
|
||||
import {
|
||||
buildServiceTokenProjectPermission,
|
||||
projectAdminPermissions,
|
||||
projectMemberPermissions,
|
||||
projectNoAccessPermissions,
|
||||
ProjectPermissionSet,
|
||||
projectViewerPermission
|
||||
} from "./project-permission";
|
||||
import { buildServiceTokenProjectPermission, ProjectPermissionSet } from "./project-permission";
|
||||
|
||||
type TPermissionServiceFactoryDep = {
|
||||
orgRoleDAL: Pick<TOrgRoleDALFactory, "findOne">;
|
||||
@@ -95,6 +96,10 @@ export const permissionServiceFactory = ({
|
||||
return projectViewerPermission;
|
||||
case ProjectMembershipRole.NoAccess:
|
||||
return projectNoAccessPermissions;
|
||||
case ProjectMembershipRole.SshHostBootstrapper:
|
||||
return sshHostBootstrapPermissions;
|
||||
case ProjectMembershipRole.KmsCryptographicOperator:
|
||||
return cryptographicOperatorPermissions;
|
||||
case ProjectMembershipRole.Custom: {
|
||||
return unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(
|
||||
permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]
|
||||
@@ -625,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,
|
||||
@@ -634,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."),
|
||||
@@ -678,403 +725,6 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
|
||||
export type TProjectPermissionV2Schema = z.infer<typeof ProjectPermissionV2Schema>;
|
||||
|
||||
const buildAdminPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
// Admins get full access to everything
|
||||
[
|
||||
ProjectPermissionSub.SecretFolders,
|
||||
ProjectPermissionSub.SecretImports,
|
||||
ProjectPermissionSub.SecretApproval,
|
||||
ProjectPermissionSub.Role,
|
||||
ProjectPermissionSub.Integrations,
|
||||
ProjectPermissionSub.Webhooks,
|
||||
ProjectPermissionSub.ServiceTokens,
|
||||
ProjectPermissionSub.Settings,
|
||||
ProjectPermissionSub.Environments,
|
||||
ProjectPermissionSub.Tags,
|
||||
ProjectPermissionSub.AuditLogs,
|
||||
ProjectPermissionSub.IpAllowList,
|
||||
ProjectPermissionSub.CertificateAuthorities,
|
||||
ProjectPermissionSub.CertificateTemplates,
|
||||
ProjectPermissionSub.PkiAlerts,
|
||||
ProjectPermissionSub.PkiCollections,
|
||||
ProjectPermissionSub.SshCertificateAuthorities,
|
||||
ProjectPermissionSub.SshCertificates,
|
||||
ProjectPermissionSub.SshCertificateTemplates,
|
||||
ProjectPermissionSub.SshHostGroups
|
||||
].forEach((el) => {
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
el
|
||||
);
|
||||
});
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionCertificateActions.Edit,
|
||||
ProjectPermissionCertificateActions.Create,
|
||||
ProjectPermissionCertificateActions.Delete,
|
||||
ProjectPermissionCertificateActions.ReadPrivateKey
|
||||
],
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSshHostActions.Edit,
|
||||
ProjectPermissionSshHostActions.Read,
|
||||
ProjectPermissionSshHostActions.Create,
|
||||
ProjectPermissionSshHostActions.Delete,
|
||||
ProjectPermissionSshHostActions.IssueHostCert
|
||||
],
|
||||
ProjectPermissionSub.SshHosts
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionMemberActions.Create,
|
||||
ProjectPermissionMemberActions.Edit,
|
||||
ProjectPermissionMemberActions.Delete,
|
||||
ProjectPermissionMemberActions.Read,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionMemberActions.AssumePrivileges
|
||||
],
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionGroupActions.Create,
|
||||
ProjectPermissionGroupActions.Edit,
|
||||
ProjectPermissionGroupActions.Delete,
|
||||
ProjectPermissionGroupActions.Read,
|
||||
ProjectPermissionGroupActions.GrantPrivileges
|
||||
],
|
||||
ProjectPermissionSub.Groups
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionIdentityActions.Create,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionIdentityActions.Delete,
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionIdentityActions.AssumePrivileges
|
||||
],
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
ProjectPermissionSecretActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.Lease
|
||||
],
|
||||
ProjectPermissionSub.DynamicSecrets
|
||||
);
|
||||
|
||||
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCmekActions.Create,
|
||||
ProjectPermissionCmekActions.Edit,
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt,
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionCmekActions.Verify
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretSyncActions.Create,
|
||||
ProjectPermissionSecretSyncActions.Edit,
|
||||
ProjectPermissionSecretSyncActions.Delete,
|
||||
ProjectPermissionSecretSyncActions.Read,
|
||||
ProjectPermissionSecretSyncActions.SyncSecrets,
|
||||
ProjectPermissionSecretSyncActions.ImportSecrets,
|
||||
ProjectPermissionSecretSyncActions.RemoveSecrets
|
||||
],
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionKmipActions.CreateClients,
|
||||
ProjectPermissionKmipActions.UpdateClients,
|
||||
ProjectPermissionKmipActions.DeleteClients,
|
||||
ProjectPermissionKmipActions.ReadClients,
|
||||
ProjectPermissionKmipActions.GenerateClientCertificates
|
||||
],
|
||||
ProjectPermissionSub.Kmip
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretRotationActions.Create,
|
||||
ProjectPermissionSecretRotationActions.Edit,
|
||||
ProjectPermissionSecretRotationActions.Delete,
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
ProjectPermissionSecretRotationActions.ReadGeneratedCredentials,
|
||||
ProjectPermissionSecretRotationActions.RotateSecrets
|
||||
],
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
export const projectAdminPermissions = buildAdminPermissionRules();
|
||||
|
||||
const buildMemberPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
ProjectPermissionSecretActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.SecretFolders
|
||||
);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
||||
ProjectPermissionDynamicSecretActions.Lease
|
||||
],
|
||||
ProjectPermissionSub.DynamicSecrets
|
||||
);
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.SecretImports
|
||||
);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
||||
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
|
||||
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||
|
||||
can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member);
|
||||
|
||||
can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Integrations
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Webhooks
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionIdentityActions.Create,
|
||||
ProjectPermissionIdentityActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.ServiceTokens
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Settings
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Environments
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Tags
|
||||
);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.Role);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
|
||||
|
||||
// double check if all CRUD are needed for CA and Certificates
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCertificateActions.Read,
|
||||
ProjectPermissionCertificateActions.Edit,
|
||||
ProjectPermissionCertificateActions.Create,
|
||||
ProjectPermissionCertificateActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
|
||||
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||
|
||||
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionCmekActions.Create,
|
||||
ProjectPermissionCmekActions.Edit,
|
||||
ProjectPermissionCmekActions.Delete,
|
||||
ProjectPermissionCmekActions.Read,
|
||||
ProjectPermissionCmekActions.Encrypt,
|
||||
ProjectPermissionCmekActions.Decrypt,
|
||||
ProjectPermissionCmekActions.Sign,
|
||||
ProjectPermissionCmekActions.Verify
|
||||
],
|
||||
ProjectPermissionSub.Cmek
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretSyncActions.Create,
|
||||
ProjectPermissionSecretSyncActions.Edit,
|
||||
ProjectPermissionSecretSyncActions.Delete,
|
||||
ProjectPermissionSecretSyncActions.Read,
|
||||
ProjectPermissionSecretSyncActions.SyncSecrets,
|
||||
ProjectPermissionSecretSyncActions.ImportSecrets,
|
||||
ProjectPermissionSecretSyncActions.RemoveSecrets
|
||||
],
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
export const projectMemberPermissions = 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(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
|
||||
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
can(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
export const projectViewerPermission = buildViewerPermissionRules();
|
||||
|
||||
const buildNoAccessProjectPermission = () => {
|
||||
const { rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
return rules;
|
||||
};
|
||||
|
||||
export const buildServiceTokenProjectPermission = (
|
||||
scopes: Array<{ secretPath: string; environment: string }>,
|
||||
permission: string[]
|
||||
@@ -1116,8 +766,6 @@ export const buildServiceTokenProjectPermission = (
|
||||
return build({ conditionsMatcher });
|
||||
};
|
||||
|
||||
export const projectNoAccessPermissions = buildNoAccessProjectPermission();
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
|
@@ -1,22 +1,27 @@
|
||||
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
|
||||
import { ProjectType } from "@app/db/schemas";
|
||||
import {
|
||||
InfisicalProjectTemplate,
|
||||
TUnpackedPermission
|
||||
} from "@app/ee/services/project-template/project-template-types";
|
||||
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
|
||||
|
||||
export const getDefaultProjectTemplate = (orgId: string) => ({
|
||||
import { ProjectTemplateDefaultEnvironments } from "./project-template-constants";
|
||||
|
||||
export const getDefaultProjectTemplate = (orgId: string, type: ProjectType) => ({
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod
|
||||
type,
|
||||
name: InfisicalProjectTemplate.Default,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
description: "Infisical's default project template",
|
||||
environments: ProjectTemplateDefaultEnvironments,
|
||||
roles: [...getPredefinedRoles("project-template")].map(({ name, slug, permissions }) => ({
|
||||
name,
|
||||
slug,
|
||||
permissions: permissions as TUnpackedPermission[]
|
||||
})),
|
||||
description: `Infisical's ${type} default project template`,
|
||||
environments: type === ProjectType.SecretManager ? ProjectTemplateDefaultEnvironments : null,
|
||||
roles: [...getPredefinedRoles({ projectId: "project-template", projectType: type })].map(
|
||||
({ name, slug, permissions }) => ({
|
||||
name,
|
||||
slug,
|
||||
permissions: permissions as TUnpackedPermission[]
|
||||
})
|
||||
),
|
||||
orgId
|
||||
});
|
||||
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
|
||||
import { TProjectTemplates } from "@app/db/schemas";
|
||||
import { ProjectType, TProjectTemplates } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
|
||||
import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
|
||||
import {
|
||||
TCreateProjectTemplateDTO,
|
||||
@@ -32,11 +33,13 @@ const $unpackProjectTemplate = ({ roles, environments, ...rest }: TProjectTempla
|
||||
...rest,
|
||||
environments: environments as TProjectTemplateEnvironment[],
|
||||
roles: [
|
||||
...getPredefinedRoles("project-template").map(({ name, slug, permissions }) => ({
|
||||
name,
|
||||
slug,
|
||||
permissions: permissions as TUnpackedPermission[]
|
||||
})),
|
||||
...getPredefinedRoles({ projectId: "project-template", projectType: rest.type as ProjectType }).map(
|
||||
({ name, slug, permissions }) => ({
|
||||
name,
|
||||
slug,
|
||||
permissions: permissions as TUnpackedPermission[]
|
||||
})
|
||||
),
|
||||
...(roles as TProjectTemplateRole[]).map((role) => ({
|
||||
...role,
|
||||
permissions: unpackPermissions(role.permissions)
|
||||
@@ -49,7 +52,7 @@ export const projectTemplateServiceFactory = ({
|
||||
permissionService,
|
||||
projectTemplateDAL
|
||||
}: TProjectTemplatesServiceFactoryDep) => {
|
||||
const listProjectTemplatesByOrg = async (actor: OrgServiceActor) => {
|
||||
const listProjectTemplatesByOrg = async (actor: OrgServiceActor, type?: ProjectType) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.projectTemplates)
|
||||
@@ -68,11 +71,14 @@ export const projectTemplateServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
||||
|
||||
const projectTemplates = await projectTemplateDAL.find({
|
||||
orgId: actor.orgId
|
||||
orgId: actor.orgId,
|
||||
...(type ? { type } : {})
|
||||
});
|
||||
|
||||
return [
|
||||
getDefaultProjectTemplate(actor.orgId),
|
||||
...(type
|
||||
? [getDefaultProjectTemplate(actor.orgId, type)]
|
||||
: Object.values(ProjectType).map((projectType) => getDefaultProjectTemplate(actor.orgId, projectType))),
|
||||
...projectTemplates.map((template) => $unpackProjectTemplate(template))
|
||||
];
|
||||
};
|
||||
@@ -134,7 +140,7 @@ export const projectTemplateServiceFactory = ({
|
||||
};
|
||||
|
||||
const createProjectTemplate = async (
|
||||
{ roles, environments, ...params }: TCreateProjectTemplateDTO,
|
||||
{ roles, environments, type, ...params }: TCreateProjectTemplateDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
@@ -154,6 +160,17 @@ export const projectTemplateServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
|
||||
|
||||
if (environments && type !== ProjectType.SecretManager) {
|
||||
throw new BadRequestError({ message: "Cannot configure environments for non-SecretManager project templates" });
|
||||
}
|
||||
|
||||
if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
message: `Failed to create project template due to environment count exceeding your current limit of ${plan.environmentLimit}. Contact Infisical to increase limit.`
|
||||
});
|
||||
}
|
||||
|
||||
const isConflictingName = Boolean(
|
||||
await projectTemplateDAL.findOne({
|
||||
name: params.name,
|
||||
@@ -169,8 +186,10 @@ export const projectTemplateServiceFactory = ({
|
||||
const projectTemplate = await projectTemplateDAL.create({
|
||||
...params,
|
||||
roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))),
|
||||
environments: JSON.stringify(environments),
|
||||
orgId: actor.orgId
|
||||
environments:
|
||||
type === ProjectType.SecretManager ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null,
|
||||
orgId: actor.orgId,
|
||||
type
|
||||
});
|
||||
|
||||
return $unpackProjectTemplate(projectTemplate);
|
||||
@@ -202,6 +221,19 @@ export const projectTemplateServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
||||
|
||||
if (projectTemplate.type !== ProjectType.SecretManager && environments)
|
||||
throw new BadRequestError({ message: "Cannot configure environments for non-SecretManager project templates" });
|
||||
|
||||
if (projectTemplate.type === ProjectType.SecretManager && environments === null)
|
||||
throw new BadRequestError({ message: "Environments cannot be removed for SecretManager project templates" });
|
||||
|
||||
if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
message: `Failed to update project template due to environment count exceeding your current limit of ${plan.environmentLimit}. Contact Infisical to increase limit.`
|
||||
});
|
||||
}
|
||||
|
||||
if (params.name && projectTemplate.name !== params.name) {
|
||||
const isConflictingName = Boolean(
|
||||
await projectTemplateDAL.findOne({
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TProjectEnvironments } from "@app/db/schemas";
|
||||
import { ProjectType, TProjectEnvironments } from "@app/db/schemas";
|
||||
import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||
|
||||
@@ -15,8 +15,9 @@ export type TProjectTemplateRole = {
|
||||
export type TCreateProjectTemplateDTO = {
|
||||
name: string;
|
||||
description?: string;
|
||||
type: ProjectType;
|
||||
roles: TProjectTemplateRole[];
|
||||
environments: TProjectTemplateEnvironment[];
|
||||
environments?: TProjectTemplateEnvironment[] | null;
|
||||
};
|
||||
|
||||
export type TUpdateProjectTemplateDTO = Partial<TCreateProjectTemplateDTO>;
|
||||
|
@@ -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"),
|
||||
|
@@ -0,0 +1,11 @@
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
|
||||
export const canUseSecretScanning = (orgId: string) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (!appCfg.isCloud) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(orgId);
|
||||
};
|
@@ -12,6 +12,7 @@ import { NotFoundError } from "@app/lib/errors";
|
||||
import { TGitAppDALFactory } from "./git-app-dal";
|
||||
import { TGitAppInstallSessionDALFactory } from "./git-app-install-session-dal";
|
||||
import { TSecretScanningDALFactory } from "./secret-scanning-dal";
|
||||
import { canUseSecretScanning } from "./secret-scanning-fns";
|
||||
import { TSecretScanningQueueFactory } from "./secret-scanning-queue";
|
||||
import {
|
||||
SecretScanningRiskStatus,
|
||||
@@ -47,12 +48,14 @@ export const secretScanningServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TInstallAppSessionDTO) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
const sessionId = crypto.randomBytes(16).toString("hex");
|
||||
await gitAppInstallSessionDAL.upsert({ orgId, sessionId, userId: actorId });
|
||||
return { sessionId };
|
||||
return { sessionId, gitAppSlug: appCfg.SECRET_SCANNING_GIT_APP_SLUG };
|
||||
};
|
||||
|
||||
const linkInstallationToOrg = async ({
|
||||
@@ -91,7 +94,8 @@ export const secretScanningServiceFactory = ({
|
||||
const {
|
||||
data: { repositories }
|
||||
} = await octokit.apps.listReposAccessibleToInstallation();
|
||||
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(actorOrgId)) {
|
||||
|
||||
if (canUseSecretScanning(actorOrgId)) {
|
||||
await Promise.all(
|
||||
repositories.map(({ id, full_name }) =>
|
||||
secretScanningQueue.startFullRepoScan({
|
||||
@@ -102,6 +106,7 @@ export const secretScanningServiceFactory = ({
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return { installatedApp };
|
||||
};
|
||||
|
||||
@@ -164,7 +169,6 @@ export const secretScanningServiceFactory = ({
|
||||
};
|
||||
|
||||
const handleRepoPushEvent = async (payload: WebhookEventMap["push"]) => {
|
||||
const appCfg = getConfig();
|
||||
const { commits, repository, installation, pusher } = payload;
|
||||
if (!commits || !repository || !installation || !pusher) {
|
||||
return;
|
||||
@@ -175,7 +179,7 @@ export const secretScanningServiceFactory = ({
|
||||
});
|
||||
if (!installationLink) return;
|
||||
|
||||
if (appCfg.SECRET_SCANNING_ORG_WHITELIST?.includes(installationLink.orgId)) {
|
||||
if (canUseSecretScanning(installationLink.orgId)) {
|
||||
await secretScanningQueue.startPushEventScan({
|
||||
commits,
|
||||
pusher: { name: pusher.name, email: pusher.email },
|
||||
|
@@ -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;
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
|
||||
import { applyJitter } from "@app/lib/dates";
|
||||
import { delay as delayMs } from "@app/lib/delay";
|
||||
import { Redlock, Settings } from "@app/lib/red-lock";
|
||||
|
||||
export const PgSqlLock = {
|
||||
@@ -48,6 +50,13 @@ export const KeyStoreTtls = {
|
||||
AccessTokenStatusUpdateInSeconds: 120
|
||||
};
|
||||
|
||||
type TDeleteItems = {
|
||||
pattern: string;
|
||||
batchSize?: number;
|
||||
delay?: number;
|
||||
jitter?: number;
|
||||
};
|
||||
|
||||
type TWaitTillReady = {
|
||||
key: string;
|
||||
waitingCb?: () => void;
|
||||
@@ -75,6 +84,35 @@ export const keyStoreFactory = (redisUrl: string) => {
|
||||
|
||||
const deleteItem = async (key: string) => redis.del(key);
|
||||
|
||||
const deleteItems = async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }: TDeleteItems) => {
|
||||
let cursor = "0";
|
||||
let totalDeleted = 0;
|
||||
|
||||
do {
|
||||
// Await in loop is needed so that Redis is not overwhelmed
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); // Count should be 1000 - 5000 for prod loads
|
||||
cursor = nextCursor;
|
||||
|
||||
for (let i = 0; i < keys.length; i += batchSize) {
|
||||
const batch = keys.slice(i, i + batchSize);
|
||||
const pipeline = redis.pipeline();
|
||||
for (const key of batch) {
|
||||
pipeline.unlink(key);
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await pipeline.exec();
|
||||
totalDeleted += batch.length;
|
||||
console.log("BATCH DONE");
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delayMs(Math.max(0, applyJitter(delay, jitter)));
|
||||
}
|
||||
} while (cursor !== "0");
|
||||
|
||||
return totalDeleted;
|
||||
};
|
||||
|
||||
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
|
||||
|
||||
const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds);
|
||||
@@ -94,7 +132,7 @@ export const keyStoreFactory = (redisUrl: string) => {
|
||||
// eslint-disable-next-line
|
||||
await new Promise((resolve) => {
|
||||
waitingCb?.();
|
||||
setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter)));
|
||||
setTimeout(resolve, Math.max(0, applyJitter(delay, jitter)));
|
||||
});
|
||||
attempts += 1;
|
||||
// eslint-disable-next-line
|
||||
@@ -108,6 +146,7 @@ export const keyStoreFactory = (redisUrl: string) => {
|
||||
setExpiry,
|
||||
setItemWithExpiry,
|
||||
deleteItem,
|
||||
deleteItems,
|
||||
incrementBy,
|
||||
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
|
||||
return redisLock.acquire(resources, duration, settings);
|
||||
|
@@ -1,3 +1,7 @@
|
||||
import RE2 from "re2";
|
||||
|
||||
import { applyJitter } from "@app/lib/dates";
|
||||
import { delay as delayMs } from "@app/lib/delay";
|
||||
import { Lock } from "@app/lib/red-lock";
|
||||
|
||||
import { TKeyStoreFactory } from "./keystore";
|
||||
@@ -19,6 +23,27 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => {
|
||||
delete store[key];
|
||||
return 1;
|
||||
},
|
||||
deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => {
|
||||
const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
||||
let totalDeleted = 0;
|
||||
const keys = Object.keys(store);
|
||||
|
||||
for (let i = 0; i < keys.length; i += batchSize) {
|
||||
const batch = keys.slice(i, i + batchSize);
|
||||
|
||||
for (const key of batch) {
|
||||
if (regex.test(key)) {
|
||||
delete store[key];
|
||||
totalDeleted += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delayMs(Math.max(0, applyJitter(delay, jitter)));
|
||||
}
|
||||
|
||||
return totalDeleted;
|
||||
},
|
||||
getItem: async (key) => {
|
||||
const value = store[key];
|
||||
if (typeof value === "string") {
|
||||
|
@@ -18,6 +18,7 @@ export enum ApiDocsTags {
|
||||
KubernetesAuth = "Kubernetes Auth",
|
||||
JwtAuth = "JWT Auth",
|
||||
OidcAuth = "OIDC Auth",
|
||||
LdapAuth = "LDAP Auth",
|
||||
Groups = "Groups",
|
||||
Organizations = "Organizations",
|
||||
Projects = "Projects",
|
||||
@@ -45,6 +46,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",
|
||||
@@ -184,6 +186,49 @@ export const UNIVERSAL_AUTH = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const LDAP_AUTH = {
|
||||
LOGIN: {
|
||||
identityId: "The ID of the identity to login.",
|
||||
username: "The username of the LDAP user to login.",
|
||||
password: "The password of the LDAP user to login."
|
||||
},
|
||||
ATTACH: {
|
||||
identityId: "The ID of the identity to attach the configuration onto.",
|
||||
url: "The URL of the LDAP server.",
|
||||
allowedFields:
|
||||
"The comma-separated array of key/value pairs of required fields that the LDAP entry must have in order to authenticate.",
|
||||
searchBase: "The base DN to search for the LDAP user.",
|
||||
searchFilter: "The filter to use to search for the LDAP user.",
|
||||
bindDN: "The DN of the user to bind to the LDAP server.",
|
||||
bindPass: "The password of the user to bind to the LDAP server.",
|
||||
ldapCaCertificate: "The PEM-encoded CA certificate for the LDAP server.",
|
||||
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 configuration for.",
|
||||
url: "The new URL of the LDAP server.",
|
||||
allowedFields: "The comma-separated list of allowed fields to return from the LDAP user.",
|
||||
searchBase: "The new base DN to search for the LDAP user.",
|
||||
searchFilter: "The new filter to use to search for the LDAP user.",
|
||||
bindDN: "The new DN of the user to bind to the LDAP server.",
|
||||
bindPass: "The new password of the user to bind to the LDAP server.",
|
||||
ldapCaCertificate: "The new PEM-encoded CA certificate for the LDAP server.",
|
||||
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 configuration for."
|
||||
},
|
||||
REVOKE: {
|
||||
identityId: "The ID of the identity to revoke the configuration for."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const AWS_AUTH = {
|
||||
LOGIN: {
|
||||
identityId: "The ID of the identity to login.",
|
||||
@@ -595,6 +640,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;
|
||||
|
||||
@@ -1434,7 +1482,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:
|
||||
@@ -1449,7 +1497,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."
|
||||
@@ -1687,6 +1735,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.",
|
||||
@@ -1822,8 +1931,12 @@ export const KMS = {
|
||||
};
|
||||
|
||||
export const ProjectTemplates = {
|
||||
LIST: {
|
||||
type: "The type of project template to list."
|
||||
},
|
||||
CREATE: {
|
||||
name: "The name of the project template to be created. Must be slug-friendly.",
|
||||
type: "The type of project template to be created.",
|
||||
description: "An optional description of the project template.",
|
||||
roles: "The roles to be created when the template is applied to a project.",
|
||||
environments: "The environments to be created when the template is applied to a project."
|
||||
@@ -1926,6 +2039,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."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2073,6 +2193,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."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -146,6 +146,7 @@ const envSchema = z
|
||||
SECRET_SCANNING_GIT_APP_ID: zpStr(z.string().optional()),
|
||||
SECRET_SCANNING_PRIVATE_KEY: zpStr(z.string().optional()),
|
||||
SECRET_SCANNING_ORG_WHITELIST: zpStr(z.string().optional()),
|
||||
SECRET_SCANNING_GIT_APP_SLUG: zpStr(z.string().default("infisical-radar")),
|
||||
// LICENSE
|
||||
LICENSE_SERVER_URL: zpStr(z.string().optional().default("https://portal.infisical.com")),
|
||||
LICENSE_SERVER_KEY: zpStr(z.string().optional()),
|
||||
|
4
backend/src/lib/delay/index.ts
Normal file
4
backend/src/lib/delay/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const delay = (ms: number) =>
|
||||
new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
@@ -84,7 +84,9 @@ const redactedKeys = [
|
||||
"secrets",
|
||||
"key",
|
||||
"password",
|
||||
"config"
|
||||
"config",
|
||||
"bindPass",
|
||||
"bindDN"
|
||||
];
|
||||
|
||||
const UNKNOWN_REQUEST_ID = "UNKNOWN_REQUEST_ID";
|
||||
|
@@ -25,6 +25,7 @@ import {
|
||||
TQueueSecretSyncSyncSecretsByIdDTO,
|
||||
TQueueSendSecretSyncActionFailedNotificationsDTO
|
||||
} from "@app/services/secret-sync/secret-sync-types";
|
||||
import { CacheType } from "@app/services/super-admin/super-admin-types";
|
||||
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
|
||||
|
||||
export enum QueueName {
|
||||
@@ -49,7 +50,8 @@ export enum QueueName {
|
||||
AccessTokenStatusUpdate = "access-token-status-update",
|
||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
||||
AppConnectionSecretSync = "app-connection-secret-sync",
|
||||
SecretRotationV2 = "secret-rotation-v2"
|
||||
SecretRotationV2 = "secret-rotation-v2",
|
||||
InvalidateCache = "invalidate-cache"
|
||||
}
|
||||
|
||||
export enum QueueJobs {
|
||||
@@ -81,7 +83,8 @@ export enum QueueJobs {
|
||||
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
|
||||
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
||||
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification"
|
||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
||||
InvalidateCache = "invalidate-cache"
|
||||
}
|
||||
|
||||
export type TQueueJobTypes = {
|
||||
@@ -234,6 +237,14 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.SecretRotationV2SendNotification;
|
||||
payload: TSecretRotationSendNotificationJobPayload;
|
||||
};
|
||||
[QueueName.InvalidateCache]: {
|
||||
name: QueueJobs.InvalidateCache;
|
||||
payload: {
|
||||
data: {
|
||||
type: CacheType;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||
|
@@ -100,3 +100,18 @@ export const publicSshCaLimit: RateLimitOptions = {
|
||||
max: 30, // conservative default
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const invalidateCacheLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
hook: "preValidation",
|
||||
max: 2,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
// Makes spamming "request access" harder, preventing email DDoS
|
||||
export const requestAccessLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
hook: "preValidation",
|
||||
max: 10,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
@@ -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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -160,6 +160,8 @@ import { identityJwtAuthDALFactory } from "@app/services/identity-jwt-auth/ident
|
||||
import { identityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/identity-jwt-auth-service";
|
||||
import { identityKubernetesAuthDALFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-dal";
|
||||
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 { 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";
|
||||
@@ -195,6 +197,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";
|
||||
@@ -242,6 +246,7 @@ import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack-
|
||||
import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
|
||||
import { slackServiceFactory } from "@app/services/slack/slack-service";
|
||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { invalidateCacheQueueFactory } from "@app/services/super-admin/invalidate-cache-queue";
|
||||
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||
import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||
import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal";
|
||||
@@ -353,6 +358,7 @@ export const registerRoutes = async (
|
||||
const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
|
||||
const identityJwtAuthDAL = identityJwtAuthDALFactory(db);
|
||||
const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
|
||||
const identityLdapAuthDAL = identityLdapAuthDALFactory(db);
|
||||
|
||||
const auditLogDAL = auditLogDALFactory(auditLogDb ?? db);
|
||||
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
||||
@@ -611,6 +617,11 @@ export const registerRoutes = async (
|
||||
queueService
|
||||
});
|
||||
|
||||
const invalidateCacheQueue = invalidateCacheQueueFactory({
|
||||
keyStore,
|
||||
queueService
|
||||
});
|
||||
|
||||
const userService = userServiceFactory({
|
||||
userDAL,
|
||||
userAliasDAL,
|
||||
@@ -722,7 +733,8 @@ export const registerRoutes = async (
|
||||
keyStore,
|
||||
licenseService,
|
||||
kmsService,
|
||||
microsoftTeamsService
|
||||
microsoftTeamsService,
|
||||
invalidateCacheQueue
|
||||
});
|
||||
|
||||
const orgAdminService = orgAdminServiceFactory({
|
||||
@@ -818,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,
|
||||
@@ -860,6 +873,8 @@ export const registerRoutes = async (
|
||||
|
||||
const sshHostService = sshHostServiceFactory({
|
||||
userDAL,
|
||||
groupDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectDAL,
|
||||
projectSshConfigDAL,
|
||||
sshCertificateAuthorityDAL,
|
||||
@@ -882,7 +897,8 @@ export const registerRoutes = async (
|
||||
sshHostLoginUserMappingDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
licenseService,
|
||||
groupDAL
|
||||
});
|
||||
|
||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||
@@ -949,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,
|
||||
@@ -1046,6 +1076,7 @@ export const registerRoutes = async (
|
||||
projectRoleDAL,
|
||||
folderDAL,
|
||||
licenseService,
|
||||
pkiSubscriberDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
pkiAlertDAL,
|
||||
@@ -1438,6 +1469,16 @@ export const registerRoutes = async (
|
||||
kmsService
|
||||
});
|
||||
|
||||
const identityLdapAuthService = identityLdapAuthServiceFactory({
|
||||
identityLdapAuthDAL,
|
||||
permissionService,
|
||||
kmsService,
|
||||
identityAccessTokenDAL,
|
||||
identityOrgMembershipDAL,
|
||||
licenseService,
|
||||
identityDAL
|
||||
});
|
||||
|
||||
const gatewayService = gatewayServiceFactory({
|
||||
permissionService,
|
||||
gatewayDAL,
|
||||
@@ -1698,6 +1739,7 @@ export const registerRoutes = async (
|
||||
identityAzureAuth: identityAzureAuthService,
|
||||
identityOidcAuth: identityOidcAuthService,
|
||||
identityJwtAuth: identityJwtAuthService,
|
||||
identityLdapAuth: identityLdapAuthService,
|
||||
accessApprovalPolicy: accessApprovalPolicyService,
|
||||
accessApprovalRequest: accessApprovalRequestService,
|
||||
secretApprovalPolicy: secretApprovalPolicyService,
|
||||
@@ -1721,6 +1763,7 @@ export const registerRoutes = async (
|
||||
certificateEst: certificateEstService,
|
||||
pkiAlert: pkiAlertService,
|
||||
pkiCollection: pkiCollectionService,
|
||||
pkiSubscriber: pkiSubscriberService,
|
||||
secretScanning: secretScanningService,
|
||||
license: licenseService,
|
||||
trustedIp: trustedIpService,
|
||||
@@ -1763,6 +1806,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", {
|
||||
|
@@ -4,13 +4,14 @@ import { z } from "zod";
|
||||
import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
|
||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
|
||||
import { CacheType, LoginMethod } from "@app/services/super-admin/super-admin-types";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -548,4 +549,69 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/invalidate-cache",
|
||||
config: {
|
||||
rateLimit: invalidateCacheLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
type: z.nativeEnum(CacheType)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: (req, res, done) => {
|
||||
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
|
||||
verifySuperAdmin(req, res, done);
|
||||
});
|
||||
},
|
||||
handler: async (req) => {
|
||||
await server.services.superAdmin.invalidateCache(req.body.type);
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.InvalidateCache,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: "Cache invalidation job started"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/invalidating-cache-status",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
invalidating: z.boolean()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: (req, res, done) => {
|
||||
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
|
||||
verifySuperAdmin(req, res, done);
|
||||
});
|
||||
},
|
||||
handler: async () => {
|
||||
const invalidating = await server.services.superAdmin.checkIfInvalidatingCache();
|
||||
|
||||
return {
|
||||
invalidating
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
497
backend/src/server/routes/v1/identity-ldap-auth-router.ts
Normal file
497
backend/src/server/routes/v1/identity-ldap-auth-router.ts
Normal file
@@ -0,0 +1,497 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
// All the any rules are disabled because passport typesense with fastify is really poor
|
||||
|
||||
import { Authenticator } from "@fastify/passport";
|
||||
import fastifySession from "@fastify/session";
|
||||
import { FastifyRequest } from "fastify";
|
||||
import { IncomingMessage } from "http";
|
||||
import LdapStrategy from "passport-ldapauth";
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityLdapAuthsSchema } from "@app/db/schemas/identity-ldap-auths";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { isValidLdapFilter } from "@app/ee/services/ldap-config/ldap-fns";
|
||||
import { ApiDocsTags, LDAP_AUTH } from "@app/lib/api-docs";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
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 { AllowedFieldsSchema } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
|
||||
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
|
||||
|
||||
export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
const passport = new Authenticator({ key: "ldap-identity-auth", userProperty: "passportMachineIdentity" });
|
||||
await server.register(fastifySession, { secret: appCfg.COOKIE_SECRET_SIGN_KEY });
|
||||
await server.register(passport.initialize());
|
||||
await server.register(passport.secureSession());
|
||||
|
||||
const getLdapPassportOpts = (req: FastifyRequest, done: any) => {
|
||||
const { identityId } = req.body as {
|
||||
identityId: string;
|
||||
};
|
||||
|
||||
process.nextTick(async () => {
|
||||
try {
|
||||
const { ldapConfig, opts } = await server.services.identityLdapAuth.getLdapConfig(identityId);
|
||||
req.ldapConfig = {
|
||||
...ldapConfig,
|
||||
isActive: true,
|
||||
groupSearchBase: "",
|
||||
uniqueUserAttribute: "",
|
||||
groupSearchFilter: ""
|
||||
};
|
||||
|
||||
done(null, opts);
|
||||
} catch (err) {
|
||||
logger.error(err, "Error in LDAP verification callback");
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
passport.use(
|
||||
new LdapStrategy(
|
||||
getLdapPassportOpts as any,
|
||||
// eslint-disable-next-line
|
||||
async (req: IncomingMessage, user, cb) => {
|
||||
try {
|
||||
const requestBody = (req as unknown as FastifyRequest).body as {
|
||||
username: string;
|
||||
password: string;
|
||||
identityId: string;
|
||||
};
|
||||
|
||||
if (!requestBody.username || !requestBody.password) {
|
||||
return cb(new UnauthorizedError({ message: "Invalid request. Missing username or password." }), false);
|
||||
}
|
||||
|
||||
if (!requestBody.identityId) {
|
||||
return cb(new UnauthorizedError({ message: "Invalid request. Missing identity ID." }), false);
|
||||
}
|
||||
|
||||
const { ldapConfig } = req as unknown as FastifyRequest;
|
||||
|
||||
if (ldapConfig.allowedFields) {
|
||||
for (const field of ldapConfig.allowedFields) {
|
||||
if (!user[field.key]) {
|
||||
return cb(
|
||||
new UnauthorizedError({ message: `Invalid request. Missing field ${field.key} on user.` }),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
const value = field.value.split(",");
|
||||
|
||||
if (!value.includes(user[field.key])) {
|
||||
return cb(
|
||||
new UnauthorizedError({
|
||||
message: `Invalid request. User field '${field.key}' does not match required fields.`
|
||||
}),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cb(null, { identityId: requestBody.identityId, user });
|
||||
} catch (error) {
|
||||
logger.error(error, "Error in LDAP verification callback");
|
||||
return cb(error, false);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/ldap-auth/login",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.LdapAuth],
|
||||
description: "Login with LDAP Auth",
|
||||
body: z.object({
|
||||
identityId: z.string().trim().describe(LDAP_AUTH.LOGIN.identityId),
|
||||
username: z.string().describe(LDAP_AUTH.LOGIN.username),
|
||||
password: z.string().describe(LDAP_AUTH.LOGIN.password)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
accessToken: z.string(),
|
||||
expiresIn: z.coerce.number(),
|
||||
accessTokenMaxTTL: z.coerce.number(),
|
||||
tokenType: z.literal("Bearer")
|
||||
})
|
||||
}
|
||||
},
|
||||
preValidation: passport.authenticate("ldapauth", {
|
||||
failWithError: true,
|
||||
session: false
|
||||
}) as any,
|
||||
|
||||
errorHandler: (error) => {
|
||||
if (error.name === "AuthenticationError") {
|
||||
throw new UnauthorizedError({ message: "Invalid credentials" });
|
||||
}
|
||||
|
||||
throw error;
|
||||
},
|
||||
|
||||
handler: async (req) => {
|
||||
if (!req.passportMachineIdentity?.identityId) {
|
||||
throw new UnauthorizedError({ message: "Invalid request. Missing identity ID or LDAP entry details." });
|
||||
}
|
||||
|
||||
const { identityId, user } = req.passportMachineIdentity;
|
||||
|
||||
const { accessToken, identityLdapAuth, identityMembershipOrg } = await server.services.identityLdapAuth.login({
|
||||
identityId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg?.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_LDAP_AUTH,
|
||||
metadata: {
|
||||
identityId,
|
||||
ldapEmail: user.mail,
|
||||
ldapUsername: user.uid
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
tokenType: "Bearer" as const,
|
||||
expiresIn: identityLdapAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/ldap-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.LdapAuth],
|
||||
description: "Attach LDAP Auth configuration onto identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim().describe(LDAP_AUTH.ATTACH.identityId)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
url: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.url),
|
||||
bindDN: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindDN),
|
||||
bindPass: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindPass),
|
||||
searchBase: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.searchBase),
|
||||
searchFilter: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.default("(uid={{username}})")
|
||||
.refine(isValidLdapFilter, "Invalid LDAP search filter")
|
||||
.describe(LDAP_AUTH.ATTACH.searchFilter),
|
||||
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
|
||||
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
|
||||
accessTokenTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenTTL),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||
})
|
||||
.refine(
|
||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityLdapAuth: IdentityLdapAuthsSchema.omit({
|
||||
encryptedBindDN: true,
|
||||
encryptedBindPass: true,
|
||||
encryptedLdapCaCertificate: true
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityLdapAuth = await server.services.identityLdapAuth.attachLdapAuth({
|
||||
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: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.ADD_IDENTITY_LDAP_AUTH,
|
||||
metadata: {
|
||||
identityId: req.params.identityId,
|
||||
url: identityLdapAuth.url,
|
||||
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
|
||||
accessTokenTTL: identityLdapAuth.accessTokenTTL,
|
||||
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
||||
allowedFields: req.body.allowedFields
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityLdapAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/ldap-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.LdapAuth],
|
||||
description: "Update LDAP Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim().describe(LDAP_AUTH.UPDATE.identityId)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
url: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.url),
|
||||
bindDN: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindDN),
|
||||
bindPass: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindPass),
|
||||
searchBase: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.searchBase),
|
||||
searchFilter: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.optional()
|
||||
.refine((v) => v === undefined || isValidLdapFilter(v), "Invalid LDAP search filter")
|
||||
.describe(LDAP_AUTH.UPDATE.searchFilter),
|
||||
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.UPDATE.allowedFields),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(LDAP_AUTH.UPDATE.accessTokenTrustedIps),
|
||||
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(LDAP_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenNumUsesLimit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe(LDAP_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe(LDAP_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({
|
||||
identityLdapAuth: IdentityLdapAuthsSchema.omit({
|
||||
encryptedBindDN: true,
|
||||
encryptedBindPass: true,
|
||||
encryptedLdapCaCertificate: true
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityLdapAuth = await server.services.identityLdapAuth.updateLdapAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
identityId: req.params.identityId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_IDENTITY_LDAP_AUTH,
|
||||
metadata: {
|
||||
identityId: req.params.identityId,
|
||||
url: identityLdapAuth.url,
|
||||
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
|
||||
accessTokenTTL: identityLdapAuth.accessTokenTTL,
|
||||
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
allowedFields: req.body.allowedFields
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityLdapAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/ldap-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.LdapAuth],
|
||||
description: "Retrieve LDAP Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim().describe(LDAP_AUTH.RETRIEVE.identityId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityLdapAuth: IdentityLdapAuthsSchema.omit({
|
||||
encryptedBindDN: true,
|
||||
encryptedBindPass: true,
|
||||
encryptedLdapCaCertificate: true
|
||||
}).extend({
|
||||
bindDN: z.string(),
|
||||
bindPass: z.string(),
|
||||
ldapCaCertificate: z.string().optional()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityLdapAuth = await server.services.identityLdapAuth.getLdapAuth({
|
||||
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: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.GET_IDENTITY_LDAP_AUTH,
|
||||
metadata: {
|
||||
identityId: identityLdapAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityLdapAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/ldap-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.LdapAuth],
|
||||
description: "Delete LDAP Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim().describe(LDAP_AUTH.REVOKE.identityId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityLdapAuth: IdentityLdapAuthsSchema.omit({
|
||||
encryptedBindDN: true,
|
||||
encryptedBindPass: true,
|
||||
encryptedLdapCaCertificate: true
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityLdapAuth = await server.services.identityLdapAuth.revokeIdentityLdapAuth({
|
||||
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: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.REVOKE_IDENTITY_LDAP_AUTH,
|
||||
metadata: {
|
||||
identityId: identityLdapAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityLdapAuth };
|
||||
}
|
||||
});
|
||||
};
|
@@ -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()
|
||||
|
@@ -19,6 +19,7 @@ import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
|
||||
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 { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
|
||||
import { registerIdentityRouter } from "./identity-router";
|
||||
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
|
||||
@@ -32,6 +33,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";
|
||||
@@ -63,6 +65,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await authRouter.register(registerIdentityAzureAuthRouter);
|
||||
await authRouter.register(registerIdentityOidcAuthRouter);
|
||||
await authRouter.register(registerIdentityJwtAuthRouter);
|
||||
await authRouter.register(registerIdentityLdapAuthRouter);
|
||||
},
|
||||
{ prefix: "/auth" }
|
||||
);
|
||||
@@ -103,6 +106,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" }
|
||||
);
|
||||
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
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";
|
||||
|
||||
@@ -47,7 +47,7 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/projects/:projectId/grant-admin-access",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
|
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
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
@@ -19,7 +19,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { re2Validator } from "@app/lib/zod";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { readLimit, requestAccessLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
import { validateMicrosoftTeamsChannelsSchema } from "@app/services/microsoft-teams/microsoft-teams-fns";
|
||||
@@ -1006,7 +1006,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/:workspaceId/project-access",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
rateLimit: requestAccessLimit
|
||||
},
|
||||
schema: {
|
||||
params: 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) => {
|
||||
|
@@ -24,6 +24,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
||||
import { sanitizedPkiSubscriber } from "@app/services/pki-subscriber/pki-subscriber-schema";
|
||||
import { ProjectFilterType } from "@app/services/project/project-types";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
@@ -170,7 +171,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
.optional()
|
||||
.default(InfisicalProjectTemplate.Default)
|
||||
.describe(PROJECTS.CREATE.template),
|
||||
type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager)
|
||||
type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager),
|
||||
shouldCreateDefaultEnvs: z.boolean().optional().default(true)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -190,7 +192,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
slug: req.body.slug,
|
||||
kmsKeyId: req.body.kmsKeyId,
|
||||
template: req.body.template,
|
||||
type: req.body.type
|
||||
type: req.body.type,
|
||||
createDefaultEnvs: req.body.shouldCreateDefaultEnvs
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
@@ -272,7 +275,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to get.")
|
||||
slug: slugSchema({ max: 36 }).describe("The slug of the project to get.")
|
||||
}),
|
||||
response: {
|
||||
200: projectWithEnv
|
||||
@@ -488,6 +491,38 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectId/pki-subscribers",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSubscribers],
|
||||
params: z.object({
|
||||
projectId: z.string().trim().describe(PROJECTS.LIST_PKI_SUBSCRIBERS.projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
subscribers: z.array(sanitizedPkiSubscriber)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const subscribers = await server.services.project.listProjectPkiSubscribers({
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
projectId: req.params.projectId
|
||||
});
|
||||
|
||||
return { subscribers };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:projectId/certificate-templates",
|
||||
@@ -626,6 +661,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHosts],
|
||||
params: z.object({
|
||||
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOSTS.projectId)
|
||||
}),
|
||||
@@ -664,6 +701,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.SshHostGroups],
|
||||
params: z.object({
|
||||
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOST_GROUPS.projectId)
|
||||
}),
|
||||
|
@@ -16,7 +16,8 @@ export enum AppConnection {
|
||||
Auth0 = "auth0",
|
||||
HCVault = "hashicorp-vault",
|
||||
LDAP = "ldap",
|
||||
TeamCity = "teamcity"
|
||||
TeamCity = "teamcity",
|
||||
OCI = "oci"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
@@ -53,6 +53,7 @@ import {
|
||||
} from "./humanitec";
|
||||
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
|
||||
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||
import { getOCIConnectionListItem, OCIConnectionMethod, validateOCIConnectionCredentials } from "./oci";
|
||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||
import {
|
||||
getTeamCityConnectionListItem,
|
||||
@@ -91,7 +92,8 @@ export const listAppConnectionOptions = () => {
|
||||
getAuth0ConnectionListItem(),
|
||||
getHCVaultConnectionListItem(),
|
||||
getLdapConnectionListItem(),
|
||||
getTeamCityConnectionListItem()
|
||||
getTeamCityConnectionListItem(),
|
||||
getOCIConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@@ -160,7 +162,8 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
@@ -176,6 +179,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case GitHubConnectionMethod.OAuth:
|
||||
return "OAuth";
|
||||
case AwsConnectionMethod.AccessKey:
|
||||
case OCIConnectionMethod.AccessKey:
|
||||
return "Access Key";
|
||||
case AwsConnectionMethod.AssumeRole:
|
||||
return "Assume Role";
|
||||
@@ -250,5 +254,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.HCVault]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
|
||||
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.OCI]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
@@ -18,5 +18,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Auth0]: "Auth0",
|
||||
[AppConnection.HCVault]: "Hashicorp Vault",
|
||||
[AppConnection.LDAP]: "LDAP",
|
||||
[AppConnection.TeamCity]: "TeamCity"
|
||||
[AppConnection.TeamCity]: "TeamCity",
|
||||
[AppConnection.OCI]: "OCI"
|
||||
};
|
||||
|
@@ -49,6 +49,8 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
|
||||
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
|
||||
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
|
||||
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { ValidateOCIConnectionCredentialsSchema } from "./oci";
|
||||
import { ociConnectionService } from "./oci/oci-connection-service";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
|
||||
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
|
||||
@@ -85,7 +87,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
|
||||
[AppConnection.HCVault]: ValidateHCVaultConnectionCredentialsSchema,
|
||||
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
|
||||
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
|
||||
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema,
|
||||
[AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -464,6 +467,7 @@ export const appConnectionServiceFactory = ({
|
||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
hcvault: hcVaultConnectionService(connectAppConnectionById),
|
||||
windmill: windmillConnectionService(connectAppConnectionById),
|
||||
teamcity: teamcityConnectionService(connectAppConnectionById)
|
||||
teamcity: teamcityConnectionService(connectAppConnectionById),
|
||||
oci: ociConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
@@ -76,6 +76,12 @@ import {
|
||||
TValidateLdapConnectionCredentialsSchema
|
||||
} from "./ldap";
|
||||
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import {
|
||||
TOCIConnection,
|
||||
TOCIConnectionConfig,
|
||||
TOCIConnectionInput,
|
||||
TValidateOCIConnectionCredentialsSchema
|
||||
} from "./oci";
|
||||
import {
|
||||
TPostgresConnection,
|
||||
TPostgresConnectionInput,
|
||||
@@ -125,6 +131,7 @@ export type TAppConnection = { id: string } & (
|
||||
| THCVaultConnection
|
||||
| TLdapConnection
|
||||
| TTeamCityConnection
|
||||
| TOCIConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -150,6 +157,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| THCVaultConnectionInput
|
||||
| TLdapConnectionInput
|
||||
| TTeamCityConnectionInput
|
||||
| TOCIConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
|
||||
@@ -180,7 +188,8 @@ export type TAppConnectionConfig =
|
||||
| TAuth0ConnectionConfig
|
||||
| THCVaultConnectionConfig
|
||||
| TLdapConnectionConfig
|
||||
| TTeamCityConnectionConfig;
|
||||
| TTeamCityConnectionConfig
|
||||
| TOCIConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -200,7 +209,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAuth0ConnectionCredentialsSchema
|
||||
| TValidateHCVaultConnectionCredentialsSchema
|
||||
| TValidateLdapConnectionCredentialsSchema
|
||||
| TValidateTeamCityConnectionCredentialsSchema;
|
||||
| TValidateTeamCityConnectionCredentialsSchema
|
||||
| TValidateOCIConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
|
4
backend/src/services/app-connection/oci/index.ts
Normal file
4
backend/src/services/app-connection/oci/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./oci-connection-enums";
|
||||
export * from "./oci-connection-fns";
|
||||
export * from "./oci-connection-schemas";
|
||||
export * from "./oci-connection-types";
|
@@ -0,0 +1,3 @@
|
||||
export enum OCIConnectionMethod {
|
||||
AccessKey = "access-key"
|
||||
}
|
139
backend/src/services/app-connection/oci/oci-connection-fns.ts
Normal file
139
backend/src/services/app-connection/oci/oci-connection-fns.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { common, identity, keymanagement } from "oci-sdk";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { OCIConnectionMethod } from "./oci-connection-enums";
|
||||
import { TOCIConnection, TOCIConnectionConfig } from "./oci-connection-types";
|
||||
|
||||
export const getOCIProvider = async (config: TOCIConnectionConfig) => {
|
||||
const {
|
||||
credentials: { fingerprint, privateKey, region, tenancyOcid, userOcid }
|
||||
} = config;
|
||||
|
||||
const provider = new common.SimpleAuthenticationDetailsProvider(
|
||||
tenancyOcid,
|
||||
userOcid,
|
||||
fingerprint,
|
||||
privateKey,
|
||||
null,
|
||||
common.Region.fromRegionId(region)
|
||||
);
|
||||
|
||||
return provider;
|
||||
};
|
||||
|
||||
export const getOCIConnectionListItem = () => {
|
||||
return {
|
||||
name: "OCI" as const,
|
||||
app: AppConnection.OCI as const,
|
||||
methods: Object.values(OCIConnectionMethod) as [OCIConnectionMethod.AccessKey]
|
||||
};
|
||||
};
|
||||
|
||||
export const validateOCIConnectionCredentials = async (config: TOCIConnectionConfig) => {
|
||||
const provider = await getOCIProvider(config);
|
||||
|
||||
try {
|
||||
const identityClient = new identity.IdentityClient({
|
||||
authenticationDetailsProvider: provider
|
||||
});
|
||||
|
||||
// Get user details - a lightweight call that validates all credentials
|
||||
await identityClient.getUser({ userId: config.credentials.userOcid });
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
||||
|
||||
export const listOCICompartments = async (appConnection: TOCIConnection) => {
|
||||
const provider = await getOCIProvider(appConnection);
|
||||
|
||||
const identityClient = new identity.IdentityClient({ authenticationDetailsProvider: provider });
|
||||
const keyManagementClient = new keymanagement.KmsVaultClient({
|
||||
authenticationDetailsProvider: provider
|
||||
});
|
||||
|
||||
const rootCompartment = await identityClient
|
||||
.getTenancy({
|
||||
tenancyId: appConnection.credentials.tenancyOcid
|
||||
})
|
||||
.then((response) => ({
|
||||
...response.tenancy,
|
||||
id: appConnection.credentials.tenancyOcid,
|
||||
name: response.tenancy.name ? `${response.tenancy.name} (root)` : "root"
|
||||
}));
|
||||
|
||||
const compartments = await identityClient.listCompartments({
|
||||
compartmentId: appConnection.credentials.tenancyOcid,
|
||||
compartmentIdInSubtree: true,
|
||||
accessLevel: identity.requests.ListCompartmentsRequest.AccessLevel.Any,
|
||||
lifecycleState: identity.models.Compartment.LifecycleState.Active
|
||||
});
|
||||
|
||||
const allCompartments = [rootCompartment, ...compartments.items];
|
||||
const filteredCompartments = [];
|
||||
|
||||
for await (const compartment of allCompartments) {
|
||||
try {
|
||||
// Check if user can list vaults in this compartment
|
||||
await keyManagementClient.listVaults({
|
||||
compartmentId: compartment.id,
|
||||
limit: 1
|
||||
});
|
||||
|
||||
filteredCompartments.push(compartment);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return filteredCompartments;
|
||||
};
|
||||
|
||||
export const listOCIVaults = async (appConnection: TOCIConnection, compartmentOcid: string) => {
|
||||
const provider = await getOCIProvider(appConnection);
|
||||
|
||||
const keyManagementClient = new keymanagement.KmsVaultClient({
|
||||
authenticationDetailsProvider: provider
|
||||
});
|
||||
|
||||
const vaults = await keyManagementClient.listVaults({
|
||||
compartmentId: compartmentOcid
|
||||
});
|
||||
|
||||
return vaults.items.filter((v) => v.lifecycleState === keymanagement.models.Vault.LifecycleState.Active);
|
||||
};
|
||||
|
||||
export const listOCIVaultKeys = async (appConnection: TOCIConnection, compartmentOcid: string, vaultOcid: string) => {
|
||||
const provider = await getOCIProvider(appConnection);
|
||||
|
||||
const kmsVaultClient = new keymanagement.KmsVaultClient({
|
||||
authenticationDetailsProvider: provider
|
||||
});
|
||||
|
||||
const vault = await kmsVaultClient.getVault({
|
||||
vaultId: vaultOcid
|
||||
});
|
||||
|
||||
const keyManagementClient = new keymanagement.KmsManagementClient({
|
||||
authenticationDetailsProvider: provider
|
||||
});
|
||||
|
||||
keyManagementClient.endpoint = vault.vault.managementEndpoint;
|
||||
|
||||
const keys = await keyManagementClient.listKeys({
|
||||
compartmentId: compartmentOcid
|
||||
});
|
||||
|
||||
return keys.items.filter((v) => v.lifecycleState === keymanagement.models.KeySummary.LifecycleState.Enabled);
|
||||
};
|
@@ -0,0 +1,65 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { OCIConnectionMethod } from "./oci-connection-enums";
|
||||
|
||||
export const OCIConnectionAccessTokenCredentialsSchema = z.object({
|
||||
userOcid: z.string().trim().min(1, "User OCID required").describe(AppConnections.CREDENTIALS.OCI.userOcid),
|
||||
tenancyOcid: z.string().trim().min(1, "Tenancy OCID required").describe(AppConnections.CREDENTIALS.OCI.tenancyOcid),
|
||||
region: z.string().trim().min(1, "Region required").describe(AppConnections.CREDENTIALS.OCI.region),
|
||||
fingerprint: z.string().trim().min(1, "Fingerprint required").describe(AppConnections.CREDENTIALS.OCI.fingerprint),
|
||||
privateKey: z.string().trim().min(1, "Private Key required").describe(AppConnections.CREDENTIALS.OCI.privateKey)
|
||||
});
|
||||
|
||||
const BaseOCIConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.OCI) });
|
||||
|
||||
export const OCIConnectionSchema = BaseOCIConnectionSchema.extend({
|
||||
method: z.literal(OCIConnectionMethod.AccessKey),
|
||||
credentials: OCIConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedOCIConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseOCIConnectionSchema.extend({
|
||||
method: z.literal(OCIConnectionMethod.AccessKey),
|
||||
credentials: OCIConnectionAccessTokenCredentialsSchema.pick({
|
||||
userOcid: true,
|
||||
tenancyOcid: true,
|
||||
region: true,
|
||||
fingerprint: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateOCIConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(OCIConnectionMethod.AccessKey).describe(AppConnections.CREATE(AppConnection.OCI).method),
|
||||
credentials: OCIConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.OCI).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateOCIConnectionSchema = ValidateOCIConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.OCI)
|
||||
);
|
||||
|
||||
export const UpdateOCIConnectionSchema = z
|
||||
.object({
|
||||
credentials: OCIConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.OCI).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OCI));
|
||||
|
||||
export const OCIConnectionListItemSchema = z.object({
|
||||
name: z.literal("OCI"),
|
||||
app: z.literal(AppConnection.OCI),
|
||||
methods: z.nativeEnum(OCIConnectionMethod).array()
|
||||
});
|
@@ -0,0 +1,70 @@
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listOCICompartments, listOCIVaultKeys, listOCIVaults } from "./oci-connection-fns";
|
||||
import { TOCIConnection } from "./oci-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TOCIConnection>;
|
||||
|
||||
type TListOCIVaultsDTO = {
|
||||
connectionId: string;
|
||||
compartmentOcid: string;
|
||||
};
|
||||
|
||||
type TListOCIVaultKeysDTO = {
|
||||
connectionId: string;
|
||||
compartmentOcid: string;
|
||||
vaultOcid: string;
|
||||
};
|
||||
|
||||
export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listCompartments = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
|
||||
|
||||
try {
|
||||
const compartments = await listOCICompartments(appConnection);
|
||||
return compartments;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to establish connection with OCI");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const listVaults = async ({ connectionId, compartmentOcid }: TListOCIVaultsDTO, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
|
||||
|
||||
try {
|
||||
const vaults = await listOCIVaults(appConnection, compartmentOcid);
|
||||
return vaults;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to establish connection with OCI");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const listVaultKeys = async (
|
||||
{ connectionId, compartmentOcid, vaultOcid }: TListOCIVaultKeysDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor);
|
||||
|
||||
try {
|
||||
const keys = await listOCIVaultKeys(appConnection, compartmentOcid, vaultOcid);
|
||||
return keys;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to establish connection with OCI");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listCompartments,
|
||||
listVaults,
|
||||
listVaultKeys
|
||||
};
|
||||
};
|
@@ -0,0 +1,22 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateOCIConnectionSchema,
|
||||
OCIConnectionSchema,
|
||||
ValidateOCIConnectionCredentialsSchema
|
||||
} from "./oci-connection-schemas";
|
||||
|
||||
export type TOCIConnection = z.infer<typeof OCIConnectionSchema>;
|
||||
|
||||
export type TOCIConnectionInput = z.infer<typeof CreateOCIConnectionSchema> & {
|
||||
app: AppConnection.OCI;
|
||||
};
|
||||
|
||||
export type TValidateOCIConnectionCredentialsSchema = typeof ValidateOCIConnectionCredentialsSchema;
|
||||
|
||||
export type TOCIConnectionConfig = DiscriminativePick<TOCIConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
@@ -1169,7 +1169,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
|
||||
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
|
||||
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
if (ca.requireTemplateForIssuance && !certificateTemplate) {
|
||||
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
||||
@@ -1520,7 +1520,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||
if (ca.status !== CaStatus.ACTIVE) throw new BadRequestError({ message: "CA is not active" });
|
||||
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
if (ca.requireTemplateForIssuance && !certificateTemplate) {
|
||||
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
||||
|
@@ -10,6 +10,18 @@ const isValidDate = (dateString: string) => {
|
||||
|
||||
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
|
||||
|
||||
export const validateAltNameField = z
|
||||
.string()
|
||||
.trim()
|
||||
.refine(
|
||||
(name) => {
|
||||
return isFQDN(name) || z.string().email().safeParse(name).success || isValidIp(name);
|
||||
},
|
||||
{
|
||||
message: "SAN must be a valid hostname, email address, or IP address"
|
||||
}
|
||||
);
|
||||
|
||||
export const validateAltNamesField = z
|
||||
.string()
|
||||
.trim()
|
||||
|
@@ -44,8 +44,27 @@ export const certificateDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const countCertificatesForPkiSubscriber = async (subscriberId: string) => {
|
||||
try {
|
||||
interface CountResult {
|
||||
count: string;
|
||||
}
|
||||
|
||||
const query = db
|
||||
.replicaNode()(TableName.Certificate)
|
||||
.where(`${TableName.Certificate}.pkiSubscriberId`, subscriberId);
|
||||
|
||||
const count = await query.count("*").first();
|
||||
|
||||
return parseInt((count as unknown as CountResult).count || "0", 10);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Count all subscriber certificates" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...certificateOrm,
|
||||
countCertificatesInProject
|
||||
countCertificatesInProject,
|
||||
countCertificatesForPkiSubscriber
|
||||
};
|
||||
};
|
||||
|
@@ -30,6 +30,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
.leftJoin(TableName.IdentityGcpAuth, `${TableName.Identity}.id`, `${TableName.IdentityGcpAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityAwsAuth, `${TableName.Identity}.id`, `${TableName.IdentityAwsAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityAzureAuth, `${TableName.Identity}.id`, `${TableName.IdentityAzureAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityLdapAuth, `${TableName.Identity}.id`, `${TableName.IdentityLdapAuth}.identityId`)
|
||||
.leftJoin(
|
||||
TableName.IdentityKubernetesAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
@@ -48,6 +49,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityLdapAuth).as("accessTokenTrustedIpsLdap"),
|
||||
db.ref("name").withSchema(TableName.Identity)
|
||||
)
|
||||
.first();
|
||||
@@ -63,7 +65,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
|
||||
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
|
||||
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
|
||||
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt
|
||||
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt,
|
||||
trustedIpsAccessLdapAuth: doc.accessTokenTrustedIpsLdap
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });
|
||||
|
@@ -186,7 +186,8 @@ export const identityAccessTokenServiceFactory = ({
|
||||
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
|
||||
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
|
||||
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth,
|
||||
[IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth
|
||||
[IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth,
|
||||
[IdentityAuthMethod.LDAP_AUTH]: identityAccessToken.trustedIpsAccessLdapAuth
|
||||
};
|
||||
|
||||
const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod];
|
||||
|
@@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TIdentityLdapAuthDALFactory = ReturnType<typeof identityLdapAuthDALFactory>;
|
||||
|
||||
export const identityLdapAuthDALFactory = (db: TDbClient) => {
|
||||
const ldapAuthOrm = ormify(db, TableName.IdentityLdapAuth);
|
||||
|
||||
return ldapAuthOrm;
|
||||
};
|
@@ -0,0 +1,543 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||
import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
|
||||
import { TIdentityLdapAuthDALFactory } from "./identity-ldap-auth-dal";
|
||||
import {
|
||||
AllowedFieldsSchema,
|
||||
TAttachLdapAuthDTO,
|
||||
TGetLdapAuthDTO,
|
||||
TLoginLdapAuthDTO,
|
||||
TRevokeLdapAuthDTO,
|
||||
TUpdateLdapAuthDTO
|
||||
} from "./identity-ldap-auth-types";
|
||||
|
||||
type TIdentityLdapAuthServiceFactoryDep = {
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
|
||||
identityLdapAuthDAL: Pick<
|
||||
TIdentityLdapAuthDALFactory,
|
||||
"findOne" | "transaction" | "create" | "updateById" | "delete"
|
||||
>;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
kmsService: TKmsServiceFactory;
|
||||
identityDAL: TIdentityDALFactory;
|
||||
};
|
||||
|
||||
export type TIdentityLdapAuthServiceFactory = ReturnType<typeof identityLdapAuthServiceFactory>;
|
||||
|
||||
export const identityLdapAuthServiceFactory = ({
|
||||
identityAccessTokenDAL,
|
||||
identityDAL,
|
||||
identityLdapAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
licenseService,
|
||||
permissionService,
|
||||
kmsService
|
||||
}: TIdentityLdapAuthServiceFactoryDep) => {
|
||||
const getLdapConfig = async (identityId: string) => {
|
||||
const identity = await identityDAL.findOne({ id: identityId });
|
||||
if (!identity) throw new NotFoundError({ message: `Identity with ID '${identityId}' not found` });
|
||||
|
||||
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: identity.id });
|
||||
if (!identityOrgMembership) throw new NotFoundError({ message: `Identity with ID '${identityId}' not found` });
|
||||
|
||||
const ldapAuth = await identityLdapAuthDAL.findOne({ identityId: identity.id });
|
||||
if (!ldapAuth) throw new NotFoundError({ message: `LDAP auth with ID '${identityId}' not found` });
|
||||
|
||||
const parsedAllowedFields = ldapAuth.allowedFields
|
||||
? AllowedFieldsSchema.array().parse(ldapAuth.allowedFields)
|
||||
: undefined;
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityOrgMembership.orgId
|
||||
});
|
||||
|
||||
const bindDN = decryptor({ cipherTextBlob: ldapAuth.encryptedBindDN }).toString();
|
||||
const bindPass = decryptor({ cipherTextBlob: ldapAuth.encryptedBindPass }).toString();
|
||||
const ldapCaCertificate = ldapAuth.encryptedLdapCaCertificate
|
||||
? decryptor({ cipherTextBlob: ldapAuth.encryptedLdapCaCertificate }).toString()
|
||||
: undefined;
|
||||
|
||||
const ldapConfig = {
|
||||
id: ldapAuth.id,
|
||||
organization: identityOrgMembership.orgId,
|
||||
url: ldapAuth.url,
|
||||
bindDN,
|
||||
bindPass,
|
||||
searchBase: ldapAuth.searchBase,
|
||||
searchFilter: ldapAuth.searchFilter,
|
||||
caCert: ldapCaCertificate || "",
|
||||
allowedFields: parsedAllowedFields
|
||||
};
|
||||
|
||||
const opts = {
|
||||
server: {
|
||||
url: ldapAuth.url,
|
||||
bindDN,
|
||||
bindCredentials: bindPass,
|
||||
searchBase: ldapAuth.searchBase,
|
||||
searchFilter: ldapAuth.searchFilter,
|
||||
...(ldapCaCertificate
|
||||
? {
|
||||
tlsOptions: {
|
||||
ca: [ldapCaCertificate]
|
||||
}
|
||||
}
|
||||
: {})
|
||||
},
|
||||
passReqToCallback: true
|
||||
};
|
||||
|
||||
return { opts, ldapConfig };
|
||||
};
|
||||
|
||||
const login = async ({ identityId }: TLoginLdapAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
|
||||
if (!identityMembershipOrg) {
|
||||
throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
}
|
||||
|
||||
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
|
||||
|
||||
if (!identityLdapAuth) {
|
||||
throw new NotFoundError({ message: `Failed to find LDAP auth for identity with ID ${identityId}` });
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
if (!plan.ldap) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to login to identity due to plan restriction. Upgrade plan to login to use LDAP authentication."
|
||||
});
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => {
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityLdapAuth.identityId,
|
||||
isAccessTokenRevoked: false,
|
||||
accessTokenTTL: identityLdapAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
||||
authMethod: IdentityAuthMethod.LDAP_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
return newToken;
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
identityId: identityLdapAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||
Number(identityAccessToken.accessTokenTTL) === 0
|
||||
? undefined
|
||||
: {
|
||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||
}
|
||||
);
|
||||
|
||||
return { accessToken, identityLdapAuth, identityAccessToken, identityMembershipOrg };
|
||||
};
|
||||
|
||||
const attachLdapAuth = async ({
|
||||
identityId,
|
||||
url,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
bindDN,
|
||||
bindPass,
|
||||
ldapCaCertificate,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId,
|
||||
isActorSuperAdmin,
|
||||
allowedFields
|
||||
}: TAttachLdapAuthDTO) => {
|
||||
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add LDAP Auth to already configured identity"
|
||||
});
|
||||
}
|
||||
|
||||
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
|
||||
if (!plan.ldap) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add LDAP Auth to identity due to plan restriction. Upgrade plan to add LDAP Auth."
|
||||
});
|
||||
}
|
||||
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||
if (
|
||||
!plan.ipAllowlisting &&
|
||||
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||
accessTokenTrustedIp.ipAddress !== "::/0"
|
||||
)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
|
||||
});
|
||||
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
||||
throw new BadRequestError({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||
});
|
||||
|
||||
if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields);
|
||||
|
||||
const identityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => {
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedBindPass } = encryptor({
|
||||
plainText: Buffer.from(bindPass)
|
||||
});
|
||||
|
||||
let encryptedLdapCaCertificate: Buffer | undefined;
|
||||
if (ldapCaCertificate) {
|
||||
const { cipherTextBlob: encryptedCertificate } = encryptor({
|
||||
plainText: Buffer.from(ldapCaCertificate)
|
||||
});
|
||||
|
||||
encryptedLdapCaCertificate = encryptedCertificate;
|
||||
}
|
||||
|
||||
const { cipherTextBlob: encryptedBindDN } = encryptor({
|
||||
plainText: Buffer.from(bindDN)
|
||||
});
|
||||
|
||||
const isConnected = await testLDAPConfig({
|
||||
bindDN,
|
||||
bindPass,
|
||||
caCert: ldapCaCertificate || "",
|
||||
url
|
||||
});
|
||||
|
||||
if (!isConnected) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to connect to LDAP server. Please ensure that the LDAP server is running and your credentials are correct."
|
||||
});
|
||||
}
|
||||
|
||||
const doc = await identityLdapAuthDAL.create(
|
||||
{
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
encryptedBindDN,
|
||||
encryptedBindPass,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
url,
|
||||
encryptedLdapCaCertificate,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
|
||||
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityLdapAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const updateLdapAuth = async ({
|
||||
identityId,
|
||||
url,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
bindDN,
|
||||
bindPass,
|
||||
ldapCaCertificate,
|
||||
allowedFields,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateLdapAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) {
|
||||
throw new NotFoundError({
|
||||
message: "The identity does not have LDAP Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
|
||||
|
||||
if (
|
||||
(accessTokenMaxTTL || identityLdapAuth.accessTokenMaxTTL) > 0 &&
|
||||
(accessTokenTTL || identityLdapAuth.accessTokenTTL) > (accessTokenMaxTTL || identityLdapAuth.accessTokenMaxTTL)
|
||||
) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
|
||||
if (!plan.ldap) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update LDAP Auth due to plan restriction. Upgrade plan to update LDAP Auth."
|
||||
});
|
||||
}
|
||||
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||
if (
|
||||
!plan.ipAllowlisting &&
|
||||
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||
accessTokenTrustedIp.ipAddress !== "::/0"
|
||||
)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
|
||||
});
|
||||
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
||||
throw new BadRequestError({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||
});
|
||||
|
||||
if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields);
|
||||
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
let encryptedBindPass: Buffer | undefined;
|
||||
if (bindPass) {
|
||||
const { cipherTextBlob: bindPassCiphertext } = encryptor({
|
||||
plainText: Buffer.from(bindPass)
|
||||
});
|
||||
|
||||
encryptedBindPass = bindPassCiphertext;
|
||||
}
|
||||
|
||||
let encryptedLdapCaCertificate: Buffer | undefined;
|
||||
if (ldapCaCertificate) {
|
||||
const { cipherTextBlob: ldapCaCertificateCiphertext } = encryptor({
|
||||
plainText: Buffer.from(ldapCaCertificate)
|
||||
});
|
||||
|
||||
encryptedLdapCaCertificate = ldapCaCertificateCiphertext;
|
||||
}
|
||||
|
||||
let encryptedBindDN: Buffer | undefined;
|
||||
if (bindDN) {
|
||||
const { cipherTextBlob: bindDNCiphertext } = encryptor({
|
||||
plainText: Buffer.from(bindDN)
|
||||
});
|
||||
|
||||
encryptedBindDN = bindDNCiphertext;
|
||||
}
|
||||
|
||||
const { ldapConfig } = await getLdapConfig(identityId);
|
||||
|
||||
const isConnected = await testLDAPConfig({
|
||||
bindDN: bindDN || ldapConfig.bindDN,
|
||||
bindPass: bindPass || ldapConfig.bindPass,
|
||||
caCert: ldapCaCertificate || ldapConfig.caCert,
|
||||
url: url || ldapConfig.url
|
||||
});
|
||||
|
||||
if (!isConnected) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to connect to LDAP server. Please ensure that the LDAP server is running and your credentials are correct."
|
||||
});
|
||||
}
|
||||
|
||||
const updatedLdapAuth = await identityLdapAuthDAL.updateById(identityLdapAuth.id, {
|
||||
url,
|
||||
searchBase,
|
||||
searchFilter,
|
||||
encryptedBindDN,
|
||||
encryptedBindPass,
|
||||
encryptedLdapCaCertificate,
|
||||
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
: undefined
|
||||
});
|
||||
|
||||
return { ...updatedLdapAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const getLdapAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetLdapAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have LDAP Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const ldapIdentityAuth = await identityLdapAuthDAL.findOne({ identityId });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const bindDN = decryptor({ cipherTextBlob: ldapIdentityAuth.encryptedBindDN }).toString();
|
||||
const bindPass = decryptor({ cipherTextBlob: ldapIdentityAuth.encryptedBindPass }).toString();
|
||||
const ldapCaCertificate = ldapIdentityAuth.encryptedLdapCaCertificate
|
||||
? decryptor({ cipherTextBlob: ldapIdentityAuth.encryptedLdapCaCertificate }).toString()
|
||||
: undefined;
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
return { ...ldapIdentityAuth, orgId: identityMembershipOrg.orgId, bindDN, bindPass, ldapCaCertificate };
|
||||
};
|
||||
|
||||
const revokeIdentityLdapAuth = async ({
|
||||
identityId,
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TRevokeLdapAuthDTO) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.LDAP_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have LDAP Auth attached"
|
||||
});
|
||||
}
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityMembershipOrg.identityId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to revoke LDAP auth of identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const revokedIdentityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => {
|
||||
const [deletedLdapAuth] = await identityLdapAuthDAL.delete({ identityId }, tx);
|
||||
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.LDAP_AUTH }, tx);
|
||||
|
||||
return { ...deletedLdapAuth, orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
return revokedIdentityLdapAuth;
|
||||
};
|
||||
|
||||
return {
|
||||
attachLdapAuth,
|
||||
getLdapConfig,
|
||||
updateLdapAuth,
|
||||
login,
|
||||
revokeIdentityLdapAuth,
|
||||
getLdapAuth
|
||||
};
|
||||
};
|
@@ -0,0 +1,56 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export const AllowedFieldsSchema = z.object({
|
||||
key: z.string().trim(),
|
||||
value: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((val) => val.replace(/\s/g, ""))
|
||||
});
|
||||
|
||||
export type TAllowedFields = z.infer<typeof AllowedFieldsSchema>;
|
||||
|
||||
export type TAttachLdapAuthDTO = {
|
||||
identityId: string;
|
||||
url: string;
|
||||
searchBase: string;
|
||||
searchFilter: string;
|
||||
bindDN: string;
|
||||
bindPass: string;
|
||||
ldapCaCertificate?: string;
|
||||
allowedFields?: TAllowedFields[];
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: { ipAddress: string }[];
|
||||
isActorSuperAdmin?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateLdapAuthDTO = {
|
||||
identityId: string;
|
||||
url?: string;
|
||||
searchBase?: string;
|
||||
searchFilter?: string;
|
||||
bindDN?: string;
|
||||
bindPass?: string;
|
||||
allowedFields?: TAllowedFields[];
|
||||
ldapCaCertificate?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetLdapAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TLoginLdapAuthDTO = {
|
||||
identityId: string;
|
||||
};
|
||||
|
||||
export type TRevokeLdapAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
@@ -8,7 +8,8 @@ export const buildAuthMethods = ({
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
jwtId
|
||||
jwtId,
|
||||
ldapId
|
||||
}: {
|
||||
uaId?: string;
|
||||
gcpId?: string;
|
||||
@@ -18,6 +19,7 @@ export const buildAuthMethods = ({
|
||||
azureId?: string;
|
||||
tokenId?: string;
|
||||
jwtId?: string;
|
||||
ldapId?: string;
|
||||
}) => {
|
||||
return [
|
||||
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
|
||||
@@ -27,6 +29,7 @@ export const buildAuthMethods = ({
|
||||
...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null],
|
||||
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
|
||||
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],
|
||||
...[jwtId ? IdentityAuthMethod.JWT_AUTH : null]
|
||||
...[jwtId ? IdentityAuthMethod.JWT_AUTH : null],
|
||||
...[ldapId ? IdentityAuthMethod.LDAP_AUTH : null]
|
||||
].filter((authMethod) => authMethod) as IdentityAuthMethod[];
|
||||
};
|
||||
|
@@ -14,6 +14,7 @@ import {
|
||||
TIdentityUniversalAuths,
|
||||
TOrgRoles
|
||||
} from "@app/db/schemas";
|
||||
import { TIdentityLdapAuths } from "@app/db/schemas/identity-ldap-auths";
|
||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
import { buildKnexFilterForSearchResource } from "@app/lib/search-resource/db";
|
||||
@@ -81,6 +82,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityJwtAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityLdapAuths>(
|
||||
TableName.IdentityLdapAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityLdapAuth}.identityId`
|
||||
)
|
||||
|
||||
.select(
|
||||
selectAllTableCols(TableName.IdentityOrgMembership),
|
||||
@@ -93,7 +99,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
|
||||
|
||||
db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth),
|
||||
db.ref("name").withSchema(TableName.Identity)
|
||||
);
|
||||
|
||||
@@ -200,6 +206,12 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityJwtAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityLdapAuths>(
|
||||
TableName.IdentityLdapAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityLdapAuth}.identityId`
|
||||
)
|
||||
|
||||
.select(
|
||||
db.ref("id").withSchema("paginatedIdentity"),
|
||||
db.ref("role").withSchema("paginatedIdentity"),
|
||||
@@ -217,7 +229,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth)
|
||||
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
|
||||
db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth)
|
||||
)
|
||||
// cr stands for custom role
|
||||
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
||||
@@ -259,6 +272,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
ldapId,
|
||||
createdAt,
|
||||
updatedAt
|
||||
}) => ({
|
||||
@@ -290,7 +304,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
jwtId
|
||||
jwtId,
|
||||
ldapId
|
||||
})
|
||||
}
|
||||
}),
|
||||
@@ -406,6 +421,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityJwtAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityLdapAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityLdapAuth}.identityId`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.IdentityOrgMembership),
|
||||
db.ref("total_count").withSchema("searchedIdentities"),
|
||||
@@ -424,7 +444,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth)
|
||||
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
|
||||
db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth)
|
||||
)
|
||||
// cr stands for custom role
|
||||
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
||||
@@ -467,6 +488,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
ldapId,
|
||||
createdAt,
|
||||
updatedAt
|
||||
}) => ({
|
||||
@@ -498,7 +520,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
jwtId
|
||||
jwtId,
|
||||
ldapId
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user