mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-02 08:27:38 +00:00
Compare commits
130 Commits
feat/addGr
...
general-oi
Author | SHA1 | Date | |
---|---|---|---|
|
3b9a50d65d | ||
|
beb7200233 | ||
|
18e3d132a2 | ||
|
6188de43e4 | ||
|
36310387e0 | ||
|
43f3960225 | ||
|
2f0a442866 | ||
|
7e05bc86a9 | ||
|
f5578d39a6 | ||
|
5a3fbc0401 | ||
|
7c52e000cd | ||
|
c8a3837432 | ||
|
2dd407b136 | ||
|
4e1a5565d8 | ||
|
d397002704 | ||
|
f5b1f671e3 | ||
|
0597c5f0c0 | ||
|
eb3afc8034 | ||
|
b67457fe93 | ||
|
75abdbe938 | ||
|
9b6a315825 | ||
|
13b2f65b7e | ||
|
6cf1e046b0 | ||
|
f6e1441dc0 | ||
|
7ed96164e5 | ||
|
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 | ||
|
091e521180 | ||
|
07df6803a5 | ||
|
d5dbc7d7e0 | ||
|
a09d0e8948 | ||
|
0af9415aa6 | ||
|
ee598560ec | ||
|
2793ac22aa | ||
|
31fad03af8 | ||
|
ce612877b8 | ||
|
4ad8b468d5 | ||
|
5742fc648b | ||
|
c629705c9c | ||
|
aa68a3ef58 | ||
|
be10f6e52a | ||
|
40c5ff0ad6 | ||
|
8ecb5ca7bc | ||
|
ab6a2b7dbb | ||
|
a757fceaed | ||
|
ce8e18f620 | ||
|
d09c964647 | ||
|
eeddbde600 | ||
|
859b643e43 | ||
|
91f71e0ef6 | ||
|
4e9e31eeb7 | ||
|
f6bc99b964 | ||
|
679eb9dffc | ||
|
0754ae3aaf | ||
|
519a0c1bdf | ||
|
e9d8979cf4 | ||
|
486d975fa0 | ||
|
42c49949b4 | ||
|
578a0d7d93 | ||
|
cd71db416d | ||
|
9d682ca874 | ||
|
9054db80ad | ||
|
5bb8756c67 | ||
|
8b7cb4c4eb | ||
|
a6ee6fc4ea | ||
|
e584c9ea95 | ||
|
b21c17572d | ||
|
44c7be54cf | ||
|
45c08b3f09 | ||
|
57a29577fe | ||
|
2700a96df4 | ||
|
7457ef3b66 | ||
|
806df70dd7 | ||
|
8eda358c17 | ||
|
b34aabe72b | ||
|
1921763fa8 | ||
|
dfaed3c513 | ||
|
5408859a18 | ||
|
5b7627585f | ||
|
800ea5ce78 | ||
|
531607dcb7 | ||
|
182de009b2 | ||
|
f1651ce171 | ||
|
e1f563dbd4 | ||
|
107cca0b62 | ||
|
72abc08f04 | ||
|
d6b31cde44 | ||
|
2c94f9ec3c | ||
|
42ad63b58d | ||
|
f2d5112585 | ||
|
9c7b25de49 | ||
|
36954a9df9 | ||
|
581840a701 | ||
|
326742c2d5 | ||
|
c891b8f5d3 | ||
|
a32bb95703 | ||
|
0410c83cef | ||
|
cf4f2ea6b1 |
@@ -28,3 +28,15 @@ frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow
|
||||
docs/cli/commands/user.mdx:generic-api-key:51
|
||||
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:76
|
||||
docs/integrations/app-connections/hashicorp-vault.mdx:generic-api-key:188
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:567
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:569
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:570
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:572
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:574
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:575
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:576
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:577
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:578
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:579
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:581
|
||||
cli/detect/config/gitleaks.toml:gcp-api-key:582
|
||||
|
@@ -133,8 +133,8 @@ RUN apt-get update && apt-get install -y \
|
||||
RUN printf "[FreeTDS]\nDescription = FreeTDS Driver\nDriver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so\nSetup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so\nFileUsage = 1\n" > /etc/odbcinst.ini
|
||||
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
|
||||
&& apt-get update && apt-get install -y infisical=0.31.1 \
|
||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
|
||||
&& apt-get update && apt-get install -y infisical=0.41.2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user
|
||||
|
@@ -127,8 +127,8 @@ RUN apt-get update && apt-get install -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
|
||||
&& apt-get update && apt-get install -y infisical=0.31.1 \
|
||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \
|
||||
&& apt-get update && apt-get install -y infisical=0.41.2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /
|
||||
|
@@ -54,8 +54,8 @@ COPY --from=build /app .
|
||||
|
||||
# Install Infisical CLI
|
||||
RUN apt-get install -y curl bash && \
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \
|
||||
apt-get update && apt-get install -y infisical=0.8.1 git
|
||||
curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
||||
apt-get update && apt-get install -y infisical=0.41.2 git
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||
CMD node healthcheck.js
|
||||
|
@@ -55,9 +55,9 @@ RUN mkdir -p /etc/softhsm2/tokens && \
|
||||
# ? App setup
|
||||
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \
|
||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
||||
apt-get update && \
|
||||
apt-get install -y infisical=0.8.1
|
||||
apt-get install -y infisical=0.41.2
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
@@ -64,9 +64,9 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \
|
||||
# ? App setup
|
||||
|
||||
# Install Infisical CLI
|
||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash && \
|
||||
RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \
|
||||
apt-get update && \
|
||||
apt-get install -y infisical=0.8.1
|
||||
apt-get install -y infisical=0.41.2
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
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"
|
||||
}
|
||||
}
|
||||
|
4
backend/src/@types/fastify.d.ts
vendored
4
backend/src/@types/fastify.d.ts
vendored
@@ -68,6 +68,7 @@ import { TIdentityJwtAuthServiceFactory } from "@app/services/identity-jwt-auth/
|
||||
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
|
||||
import { TIdentityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service";
|
||||
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
|
||||
import { TIdentityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
|
||||
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
|
||||
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
|
||||
@@ -80,6 +81,7 @@ import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
||||
import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
|
||||
import { TPkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
|
||||
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||
@@ -208,6 +210,7 @@ declare module "fastify" {
|
||||
identityGcpAuth: TIdentityGcpAuthServiceFactory;
|
||||
identityAwsAuth: TIdentityAwsAuthServiceFactory;
|
||||
identityAzureAuth: TIdentityAzureAuthServiceFactory;
|
||||
identityOciAuth: TIdentityOciAuthServiceFactory;
|
||||
identityOidcAuth: TIdentityOidcAuthServiceFactory;
|
||||
identityJwtAuth: TIdentityJwtAuthServiceFactory;
|
||||
identityLdapAuth: TIdentityLdapAuthServiceFactory;
|
||||
@@ -232,6 +235,7 @@ declare module "fastify" {
|
||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||
certificateEst: TCertificateEstServiceFactory;
|
||||
pkiCollection: TPkiCollectionServiceFactory;
|
||||
pkiSubscriber: TPkiSubscriberServiceFactory;
|
||||
secretScanning: TSecretScanningServiceFactory;
|
||||
license: TLicenseServiceFactory;
|
||||
trustedIp: TTrustedIpServiceFactory;
|
||||
|
16
backend/src/@types/knex.d.ts
vendored
16
backend/src/@types/knex.d.ts
vendored
@@ -119,6 +119,9 @@ import {
|
||||
TIdentityMetadata,
|
||||
TIdentityMetadataInsert,
|
||||
TIdentityMetadataUpdate,
|
||||
TIdentityOciAuths,
|
||||
TIdentityOciAuthsInsert,
|
||||
TIdentityOciAuthsUpdate,
|
||||
TIdentityOidcAuths,
|
||||
TIdentityOidcAuthsInsert,
|
||||
TIdentityOidcAuthsUpdate,
|
||||
@@ -209,6 +212,9 @@ import {
|
||||
TPkiCollections,
|
||||
TPkiCollectionsInsert,
|
||||
TPkiCollectionsUpdate,
|
||||
TPkiSubscribers,
|
||||
TPkiSubscribersInsert,
|
||||
TPkiSubscribersUpdate,
|
||||
TProjectBots,
|
||||
TProjectBotsInsert,
|
||||
TProjectBotsUpdate,
|
||||
@@ -564,6 +570,11 @@ declare module "knex/types/tables" {
|
||||
TPkiCollectionItemsInsert,
|
||||
TPkiCollectionItemsUpdate
|
||||
>;
|
||||
[TableName.PkiSubscriber]: KnexOriginal.CompositeTableType<
|
||||
TPkiSubscribers,
|
||||
TPkiSubscribersInsert,
|
||||
TPkiSubscribersUpdate
|
||||
>;
|
||||
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
||||
TUserGroupMembership,
|
||||
TUserGroupMembershipInsert,
|
||||
@@ -730,6 +741,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityAzureAuthsInsert,
|
||||
TIdentityAzureAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityOciAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityOciAuths,
|
||||
TIdentityOciAuthsInsert,
|
||||
TIdentityOciAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityOidcAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityOidcAuths,
|
||||
TIdentityOidcAuthsInsert,
|
||||
|
46
backend/src/db/migrations/20250508160957_pki-subscriber.ts
Normal file
46
backend/src/db/migrations/20250508160957_pki-subscriber.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.PkiSubscriber))) {
|
||||
await knex.schema.createTable(TableName.PkiSubscriber, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.timestamps(true, true, true);
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.uuid("caId").nullable();
|
||||
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("SET NULL");
|
||||
t.string("name").notNullable();
|
||||
t.string("commonName").notNullable();
|
||||
t.specificType("subjectAlternativeNames", "text[]").notNullable();
|
||||
t.string("ttl").notNullable();
|
||||
t.specificType("keyUsages", "text[]").notNullable();
|
||||
t.specificType("extendedKeyUsages", "text[]").notNullable();
|
||||
t.string("status").notNullable(); // active / disabled
|
||||
t.unique(["projectId", "name"]);
|
||||
});
|
||||
await createOnUpdateTrigger(knex, TableName.PkiSubscriber);
|
||||
}
|
||||
|
||||
const hasSubscriberCol = await knex.schema.hasColumn(TableName.Certificate, "pkiSubscriberId");
|
||||
if (!hasSubscriberCol) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.uuid("pkiSubscriberId").nullable();
|
||||
t.foreign("pkiSubscriberId").references("id").inTable(TableName.PkiSubscriber).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasSubscriberCol = await knex.schema.hasColumn(TableName.Certificate, "pkiSubscriberId");
|
||||
if (hasSubscriberCol) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.dropColumn("pkiSubscriberId");
|
||||
});
|
||||
}
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.PkiSubscriber);
|
||||
await dropOnUpdateTrigger(knex, TableName.PkiSubscriber);
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.IdentityOciAuth))) {
|
||||
await knex.schema.createTable(TableName.IdentityOciAuth, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
|
||||
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
|
||||
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
|
||||
t.jsonb("accessTokenTrustedIps").notNullable();
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("identityId").notNullable().unique();
|
||||
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
t.string("type").notNullable();
|
||||
|
||||
t.string("tenancyOcid").notNullable();
|
||||
t.string("allowedUsernames").nullable();
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.IdentityOciAuth);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityOciAuth);
|
||||
await dropOnUpdateTrigger(knex, TableName.IdentityOciAuth);
|
||||
}
|
@@ -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>;
|
||||
|
26
backend/src/db/schemas/identity-oci-auths.ts
Normal file
26
backend/src/db/schemas/identity-oci-auths.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const IdentityOciAuthsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
accessTokenTTL: z.coerce.number().default(7200),
|
||||
accessTokenMaxTTL: z.coerce.number().default(7200),
|
||||
accessTokenNumUsesLimit: z.coerce.number().default(0),
|
||||
accessTokenTrustedIps: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
identityId: z.string().uuid(),
|
||||
type: z.string(),
|
||||
tenancyOcid: z.string(),
|
||||
allowedUsernames: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TIdentityOciAuths = z.infer<typeof IdentityOciAuthsSchema>;
|
||||
export type TIdentityOciAuthsInsert = Omit<z.input<typeof IdentityOciAuthsSchema>, TImmutableDBKeys>;
|
||||
export type TIdentityOciAuthsUpdate = Partial<Omit<z.input<typeof IdentityOciAuthsSchema>, TImmutableDBKeys>>;
|
@@ -37,6 +37,7 @@ export * from "./identity-gcp-auths";
|
||||
export * from "./identity-jwt-auths";
|
||||
export * from "./identity-kubernetes-auths";
|
||||
export * from "./identity-metadata";
|
||||
export * from "./identity-oci-auths";
|
||||
export * from "./identity-oidc-auths";
|
||||
export * from "./identity-org-memberships";
|
||||
export * from "./identity-project-additional-privilege";
|
||||
@@ -69,6 +70,7 @@ export * from "./organizations";
|
||||
export * from "./pki-alerts";
|
||||
export * from "./pki-collection-items";
|
||||
export * from "./pki-collections";
|
||||
export * from "./pki-subscribers";
|
||||
export * from "./project-bots";
|
||||
export * from "./project-environments";
|
||||
export * from "./project-gateways";
|
||||
|
@@ -21,6 +21,7 @@ export enum TableName {
|
||||
CertificateBody = "certificate_bodies",
|
||||
CertificateSecret = "certificate_secrets",
|
||||
CertificateTemplate = "certificate_templates",
|
||||
PkiSubscriber = "pki_subscribers",
|
||||
PkiAlert = "pki_alerts",
|
||||
PkiCollection = "pki_collections",
|
||||
PkiCollectionItem = "pki_collection_items",
|
||||
@@ -78,6 +79,7 @@ export enum TableName {
|
||||
IdentityAzureAuth = "identity_azure_auths",
|
||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||
IdentityAwsAuth = "identity_aws_auths",
|
||||
IdentityOciAuth = "identity_oci_auths",
|
||||
IdentityOidcAuth = "identity_oidc_auths",
|
||||
IdentityJwtAuth = "identity_jwt_auths",
|
||||
IdentityLdapAuth = "identity_ldap_auths",
|
||||
@@ -232,6 +234,7 @@ export enum IdentityAuthMethod {
|
||||
GCP_AUTH = "gcp-auth",
|
||||
AWS_AUTH = "aws-auth",
|
||||
AZURE_AUTH = "azure-auth",
|
||||
OCI_AUTH = "oci-auth",
|
||||
OIDC_AUTH = "oidc-auth",
|
||||
JWT_AUTH = "jwt-auth",
|
||||
LDAP_AUTH = "ldap-auth"
|
||||
|
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>>;
|
@@ -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()
|
||||
|
@@ -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()),
|
||||
|
@@ -97,7 +97,7 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
|
||||
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
|
||||
})
|
||||
.refine((data) => ms(data.maxTTL) >= ms(data.ttl), {
|
||||
message: "Max TLL must be greater than or equal to TTL",
|
||||
message: "Max TTL must be greater than or equal to TTL",
|
||||
path: ["maxTTL"]
|
||||
}),
|
||||
response: {
|
||||
|
@@ -73,7 +73,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const host = await server.services.sshHost.getSshHost({
|
||||
const host = await server.services.sshHost.getSshHostById({
|
||||
sshHostId: req.params.sshHostId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
|
@@ -19,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 {
|
||||
@@ -34,7 +35,6 @@ import { WorkflowIntegration } from "@app/services/workflow-integration/workflow
|
||||
|
||||
import { KmipPermission } from "../kmip/kmip-enum";
|
||||
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
||||
import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-auth-types";
|
||||
|
||||
export type TListProjectAuditLogDTO = {
|
||||
filter: {
|
||||
@@ -162,6 +162,12 @@ export enum EventType {
|
||||
REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth",
|
||||
GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth",
|
||||
|
||||
LOGIN_IDENTITY_OCI_AUTH = "login-identity-oci-auth",
|
||||
ADD_IDENTITY_OCI_AUTH = "add-identity-oci-auth",
|
||||
UPDATE_IDENTITY_OCI_AUTH = "update-identity-oci-auth",
|
||||
REVOKE_IDENTITY_OCI_AUTH = "revoke-identity-oci-auth",
|
||||
GET_IDENTITY_OCI_AUTH = "get-identity-oci-auth",
|
||||
|
||||
LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth",
|
||||
ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth",
|
||||
UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth",
|
||||
@@ -254,6 +260,13 @@ export enum EventType {
|
||||
GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items",
|
||||
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
|
||||
DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item",
|
||||
CREATE_PKI_SUBSCRIBER = "create-pki-subscriber",
|
||||
UPDATE_PKI_SUBSCRIBER = "update-pki-subscriber",
|
||||
DELETE_PKI_SUBSCRIBER = "delete-pki-subscriber",
|
||||
GET_PKI_SUBSCRIBER = "get-pki-subscriber",
|
||||
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
|
||||
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
|
||||
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
|
||||
CREATE_KMS = "create-kms",
|
||||
UPDATE_KMS = "update-kms",
|
||||
DELETE_KMS = "delete-kms",
|
||||
@@ -1002,6 +1015,55 @@ interface GetIdentityAwsAuthEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityOciAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_OCI_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
identityOciAuthId: string;
|
||||
identityAccessTokenId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddIdentityOciAuthEvent {
|
||||
type: EventType.ADD_IDENTITY_OCI_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
tenancyOcid: string;
|
||||
allowedUsernames: string | null;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteIdentityOciAuthEvent {
|
||||
type: EventType.REVOKE_IDENTITY_OCI_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateIdentityOciAuthEvent {
|
||||
type: EventType.UPDATE_IDENTITY_OCI_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
tenancyOcid?: string;
|
||||
allowedUsernames: string | null;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetIdentityOciAuthEvent {
|
||||
type: EventType.GET_IDENTITY_OCI_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityAzureAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_AZURE_AUTH;
|
||||
metadata: {
|
||||
@@ -1965,6 +2027,77 @@ interface DeletePkiCollectionItem {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreatePkiSubscriber {
|
||||
type: EventType.CREATE_PKI_SUBSCRIBER;
|
||||
metadata: {
|
||||
pkiSubscriberId: string;
|
||||
caId?: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
ttl: string;
|
||||
subjectAlternativeNames: string[];
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdatePkiSubscriber {
|
||||
type: EventType.UPDATE_PKI_SUBSCRIBER;
|
||||
metadata: {
|
||||
pkiSubscriberId: string;
|
||||
caId?: string;
|
||||
name?: string;
|
||||
commonName?: string;
|
||||
ttl?: string;
|
||||
subjectAlternativeNames?: string[];
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DeletePkiSubscriber {
|
||||
type: EventType.DELETE_PKI_SUBSCRIBER;
|
||||
metadata: {
|
||||
pkiSubscriberId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetPkiSubscriber {
|
||||
type: EventType.GET_PKI_SUBSCRIBER;
|
||||
metadata: {
|
||||
pkiSubscriberId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface IssuePkiSubscriberCert {
|
||||
type: EventType.ISSUE_PKI_SUBSCRIBER_CERT;
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
name: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SignPkiSubscriberCert {
|
||||
type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
name: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ListPkiSubscriberCerts {
|
||||
type: EventType.LIST_PKI_SUBSCRIBER_CERTS;
|
||||
metadata: {
|
||||
subscriberId: string;
|
||||
name: string;
|
||||
projectId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateKmsEvent {
|
||||
type: EventType.CREATE_KMS;
|
||||
metadata: {
|
||||
@@ -2836,6 +2969,11 @@ export type Event =
|
||||
| UpdateIdentityAwsAuthEvent
|
||||
| GetIdentityAwsAuthEvent
|
||||
| DeleteIdentityAwsAuthEvent
|
||||
| LoginIdentityOciAuthEvent
|
||||
| AddIdentityOciAuthEvent
|
||||
| UpdateIdentityOciAuthEvent
|
||||
| GetIdentityOciAuthEvent
|
||||
| DeleteIdentityOciAuthEvent
|
||||
| LoginIdentityAzureAuthEvent
|
||||
| AddIdentityAzureAuthEvent
|
||||
| DeleteIdentityAzureAuthEvent
|
||||
@@ -2928,6 +3066,13 @@ export type Event =
|
||||
| GetPkiCollectionItems
|
||||
| AddPkiCollectionItem
|
||||
| DeletePkiCollectionItem
|
||||
| CreatePkiSubscriber
|
||||
| UpdatePkiSubscriber
|
||||
| DeletePkiSubscriber
|
||||
| GetPkiSubscriber
|
||||
| IssuePkiSubscriberCert
|
||||
| SignPkiSubscriberCert
|
||||
| ListPkiSubscriberCerts
|
||||
| CreateKmsEvent
|
||||
| UpdateKmsEvent
|
||||
| DeleteKmsEvent
|
||||
|
@@ -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;
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSecretSyncActions,
|
||||
@@ -76,6 +77,18 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.SshHosts
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionPkiSubscriberActions.Edit,
|
||||
ProjectPermissionPkiSubscriberActions.Read,
|
||||
ProjectPermissionPkiSubscriberActions.Create,
|
||||
ProjectPermissionPkiSubscriberActions.Delete,
|
||||
ProjectPermissionPkiSubscriberActions.IssueCert,
|
||||
ProjectPermissionPkiSubscriberActions.ListCerts
|
||||
],
|
||||
ProjectPermissionSub.PkiSubscribers
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionMemberActions.Create,
|
||||
@@ -113,7 +126,6 @@ const buildAdminPermissionRules = () => {
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Create,
|
||||
@@ -194,7 +206,6 @@ const buildMemberPermissionRules = () => {
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
ProjectPermissionSecretActions.DescribeSecret,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
@@ -338,6 +349,7 @@ const buildMemberPermissionRules = () => {
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||
|
||||
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
|
||||
can([ProjectPermissionPkiSubscriberActions.Read], ProjectPermissionSub.PkiSubscribers);
|
||||
|
||||
can(
|
||||
[
|
||||
@@ -372,9 +384,10 @@ const buildMemberPermissionRules = () => {
|
||||
const buildViewerPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
|
||||
can(
|
||||
[ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue],
|
||||
ProjectPermissionSub.Secrets
|
||||
);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
|
||||
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
||||
|
@@ -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."),
|
||||
|
@@ -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"),
|
||||
|
@@ -186,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) {
|
||||
|
@@ -335,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({
|
||||
@@ -631,7 +631,7 @@ export const sshHostServiceFactory = ({
|
||||
createSshHost,
|
||||
updateSshHost,
|
||||
deleteSshHost,
|
||||
getSshHost,
|
||||
getSshHostById,
|
||||
issueSshHostUserCert,
|
||||
issueSshHostHostCert,
|
||||
getSshHostUserCaPk,
|
||||
|
@@ -14,6 +14,7 @@ export enum ApiDocsTags {
|
||||
UniversalAuth = "Universal Auth",
|
||||
GcpAuth = "GCP Auth",
|
||||
AwsAuth = "AWS Auth",
|
||||
OciAuth = "OCI Auth",
|
||||
AzureAuth = "Azure Auth",
|
||||
KubernetesAuth = "Kubernetes Auth",
|
||||
JwtAuth = "JWT Auth",
|
||||
@@ -46,6 +47,7 @@ export enum ApiDocsTags {
|
||||
PkiCertificateTemplates = "PKI Certificate Templates",
|
||||
PkiCertificateCollections = "PKI Certificate Collections",
|
||||
PkiAlerting = "PKI Alerting",
|
||||
PkiSubscribers = "PKI Subscribers",
|
||||
SshCertificates = "SSH Certificates",
|
||||
SshCertificateAuthorities = "SSH Certificate Authorities",
|
||||
SshCertificateTemplates = "SSH Certificate Templates",
|
||||
@@ -270,6 +272,40 @@ export const AWS_AUTH = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const OCI_AUTH = {
|
||||
LOGIN: {
|
||||
identityId: "The ID of the identity to login.",
|
||||
userOcid: "The OCID of the user attempting login.",
|
||||
headers: "The headers of the signed request."
|
||||
},
|
||||
ATTACH: {
|
||||
identityId: "The ID of the identity to attach the configuration onto.",
|
||||
tenancyOcid: "The OCID of your tenancy.",
|
||||
allowedUsernames:
|
||||
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
|
||||
accessTokenTTL: "The lifetime for an access token in seconds.",
|
||||
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
|
||||
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.",
|
||||
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
|
||||
},
|
||||
UPDATE: {
|
||||
identityId: "The ID of the identity to update the auth method for.",
|
||||
tenancyOcid: "The OCID of your tenancy.",
|
||||
allowedUsernames:
|
||||
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
|
||||
accessTokenTTL: "The new lifetime for an access token in seconds.",
|
||||
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
|
||||
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
|
||||
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
|
||||
},
|
||||
RETRIEVE: {
|
||||
identityId: "The ID of the identity to retrieve the auth method for."
|
||||
},
|
||||
REVOKE: {
|
||||
identityId: "The ID of the identity to revoke the auth method for."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const AZURE_AUTH = {
|
||||
LOGIN: {
|
||||
identityId: "The ID of the identity to login."
|
||||
@@ -639,6 +675,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;
|
||||
|
||||
@@ -1731,6 +1770,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.",
|
||||
@@ -1974,6 +2074,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."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2121,6 +2228,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."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -104,6 +104,14 @@ export const publicSshCaLimit: RateLimitOptions = {
|
||||
export const invalidateCacheLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
hook: "preValidation",
|
||||
max: 1,
|
||||
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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -162,6 +162,8 @@ import { identityKubernetesAuthDALFactory } from "@app/services/identity-kuberne
|
||||
import { identityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
|
||||
import { identityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal";
|
||||
import { identityLdapAuthServiceFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-service";
|
||||
import { identityOciAuthDALFactory } from "@app/services/identity-oci-auth/identity-oci-auth-dal";
|
||||
import { identityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
|
||||
import { identityOidcAuthDALFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-dal";
|
||||
import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
|
||||
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
@@ -197,6 +199,8 @@ import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-servic
|
||||
import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||
import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
||||
import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
|
||||
import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
|
||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||
import { projectQueueFactory } from "@app/services/project/project-queue";
|
||||
import { projectServiceFactory } from "@app/services/project/project-service";
|
||||
@@ -353,6 +357,7 @@ export const registerRoutes = async (
|
||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
|
||||
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
|
||||
const identityOciAuthDAL = identityOciAuthDALFactory(db);
|
||||
const identityOidcAuthDAL = identityOidcAuthDALFactory(db);
|
||||
const identityJwtAuthDAL = identityJwtAuthDALFactory(db);
|
||||
const identityAzureAuthDAL = identityAzureAuthDALFactory(db);
|
||||
@@ -828,6 +833,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,
|
||||
@@ -962,6 +968,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,
|
||||
@@ -1059,6 +1079,7 @@ export const registerRoutes = async (
|
||||
projectRoleDAL,
|
||||
folderDAL,
|
||||
licenseService,
|
||||
pkiSubscriberDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
pkiAlertDAL,
|
||||
@@ -1433,6 +1454,14 @@ export const registerRoutes = async (
|
||||
licenseService
|
||||
});
|
||||
|
||||
const identityOciAuthService = identityOciAuthServiceFactory({
|
||||
identityAccessTokenDAL,
|
||||
identityOciAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
licenseService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const identityOidcAuthService = identityOidcAuthServiceFactory({
|
||||
identityOidcAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
@@ -1719,6 +1748,7 @@ export const registerRoutes = async (
|
||||
identityGcpAuth: identityGcpAuthService,
|
||||
identityAwsAuth: identityAwsAuthService,
|
||||
identityAzureAuth: identityAzureAuthService,
|
||||
identityOciAuth: identityOciAuthService,
|
||||
identityOidcAuth: identityOidcAuthService,
|
||||
identityJwtAuth: identityJwtAuthService,
|
||||
identityLdapAuth: identityLdapAuthService,
|
||||
@@ -1745,6 +1775,7 @@ export const registerRoutes = async (
|
||||
certificateEst: certificateEstService,
|
||||
pkiAlert: pkiAlertService,
|
||||
pkiCollection: pkiCollectionService,
|
||||
pkiSubscriber: pkiSubscriberService,
|
||||
secretScanning: secretScanningService,
|
||||
license: licenseService,
|
||||
trustedIp: trustedIpService,
|
||||
@@ -1787,6 +1818,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", {
|
||||
|
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
338
backend/src/server/routes/v1/identity-oci-auth-router.ts
Normal file
338
backend/src/server/routes/v1/identity-oci-auth-router.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityOciAuthsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, OCI_AUTH } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||
import { validateTenancy, validateUsernames } from "@app/services/identity-oci-auth/identity-oci-auth-validators";
|
||||
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
|
||||
|
||||
export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/oci-auth/login",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.OciAuth],
|
||||
description: "Login with OCI Auth",
|
||||
body: z.object({
|
||||
identityId: z.string().trim().describe(OCI_AUTH.LOGIN.identityId),
|
||||
userOcid: z.string().trim().describe(OCI_AUTH.LOGIN.userOcid),
|
||||
headers: z
|
||||
.object({
|
||||
authorization: z.string(),
|
||||
host: z.string(),
|
||||
"x-date": z.string()
|
||||
})
|
||||
.describe(OCI_AUTH.LOGIN.headers)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
accessToken: z.string(),
|
||||
expiresIn: z.coerce.number(),
|
||||
accessTokenMaxTTL: z.coerce.number(),
|
||||
tokenType: z.literal("Bearer")
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityOciAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
await server.services.identityOciAuth.login(req.body);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg?.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_OCI_AUTH,
|
||||
metadata: {
|
||||
identityId: identityOciAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
identityOciAuthId: identityOciAuth.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
tokenType: "Bearer" as const,
|
||||
expiresIn: identityOciAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/oci-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.OciAuth],
|
||||
description: "Attach OCI Auth configuration onto identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim().describe(OCI_AUTH.ATTACH.identityId)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
tenancyOcid: validateTenancy.describe(OCI_AUTH.ATTACH.tenancyOcid),
|
||||
allowedUsernames: validateUsernames.describe(OCI_AUTH.ATTACH.allowedUsernames),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||
.describe(OCI_AUTH.ATTACH.accessTokenTrustedIps),
|
||||
accessTokenTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(OCI_AUTH.ATTACH.accessTokenTTL),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(OCI_AUTH.ATTACH.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(OCI_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||
})
|
||||
.refine(
|
||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityOciAuth: IdentityOciAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityOciAuth = await server.services.identityOciAuth.attachOciAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
identityId: req.params.identityId,
|
||||
isActorSuperAdmin: isSuperAdmin(req.auth)
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityOciAuth.orgId,
|
||||
event: {
|
||||
type: EventType.ADD_IDENTITY_OCI_AUTH,
|
||||
metadata: {
|
||||
identityId: identityOciAuth.identityId,
|
||||
tenancyOcid: identityOciAuth.tenancyOcid,
|
||||
allowedUsernames: identityOciAuth.allowedUsernames || null,
|
||||
accessTokenTTL: identityOciAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityOciAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityOciAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/oci-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.OciAuth],
|
||||
description: "Update OCI Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(OCI_AUTH.UPDATE.identityId)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
tenancyOcid: validateTenancy.describe(OCI_AUTH.UPDATE.tenancyOcid),
|
||||
allowedUsernames: validateUsernames.describe(OCI_AUTH.UPDATE.allowedUsernames),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(OCI_AUTH.UPDATE.accessTokenTrustedIps),
|
||||
accessTokenTTL: z.number().int().min(0).max(315360000).optional().describe(OCI_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenNumUsesLimit: z.number().int().min(0).optional().describe(OCI_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe(OCI_AUTH.UPDATE.accessTokenMaxTTL)
|
||||
})
|
||||
.refine(
|
||||
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityOciAuth: IdentityOciAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityOciAuth = await server.services.identityOciAuth.updateOciAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
identityId: req.params.identityId,
|
||||
allowedUsernames: req.body.allowedUsernames || null
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityOciAuth.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_IDENTITY_OCI_AUTH,
|
||||
metadata: {
|
||||
identityId: identityOciAuth.identityId,
|
||||
tenancyOcid: identityOciAuth.tenancyOcid,
|
||||
allowedUsernames: identityOciAuth.allowedUsernames || null,
|
||||
accessTokenTTL: identityOciAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityOciAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityOciAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/oci-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.OciAuth],
|
||||
description: "Retrieve OCI Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(OCI_AUTH.RETRIEVE.identityId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityOciAuth: IdentityOciAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityOciAuth = await server.services.identityOciAuth.getOciAuth({
|
||||
identityId: req.params.identityId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityOciAuth.orgId,
|
||||
event: {
|
||||
type: EventType.GET_IDENTITY_OCI_AUTH,
|
||||
metadata: {
|
||||
identityId: identityOciAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
return { identityOciAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/oci-auth/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.OciAuth],
|
||||
description: "Delete OCI Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(OCI_AUTH.REVOKE.identityId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityOciAuth: IdentityOciAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityOciAuth = await server.services.identityOciAuth.revokeIdentityOciAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
identityId: req.params.identityId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityOciAuth.orgId,
|
||||
event: {
|
||||
type: EventType.REVOKE_IDENTITY_OCI_AUTH,
|
||||
metadata: {
|
||||
identityId: identityOciAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityOciAuth };
|
||||
}
|
||||
});
|
||||
};
|
@@ -52,7 +52,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
identity: IdentitiesSchema.extend({
|
||||
authMethods: z.array(z.string())
|
||||
authMethods: z.array(z.string()),
|
||||
metadata: z.object({ id: z.string(), key: z.string(), value: z.string() }).array()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -123,7 +124,9 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identity: IdentitiesSchema
|
||||
identity: IdentitiesSchema.extend({
|
||||
metadata: z.object({ id: z.string(), key: z.string(), value: z.string() }).array()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -227,8 +230,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
identity: IdentityOrgMembershipsSchema.extend({
|
||||
metadata: z
|
||||
.object({
|
||||
key: z.string().trim().min(1),
|
||||
id: z.string().trim().min(1),
|
||||
key: z.string().trim().min(1),
|
||||
value: z.string().trim().min(1)
|
||||
})
|
||||
.array()
|
||||
|
@@ -20,6 +20,7 @@ import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
|
||||
import { registerIdentityJwtAuthRouter } from "./identity-jwt-auth-router";
|
||||
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
|
||||
import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
|
||||
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
|
||||
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
|
||||
import { registerIdentityRouter } from "./identity-router";
|
||||
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
|
||||
@@ -33,6 +34,7 @@ import { registerOrgRouter } from "./organization-router";
|
||||
import { registerPasswordRouter } from "./password-router";
|
||||
import { registerPkiAlertRouter } from "./pki-alert-router";
|
||||
import { registerPkiCollectionRouter } from "./pki-collection-router";
|
||||
import { registerPkiSubscriberRouter } from "./pki-subscriber-router";
|
||||
import { registerProjectEnvRouter } from "./project-env-router";
|
||||
import { registerProjectKeyRouter } from "./project-key-router";
|
||||
import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||
@@ -62,6 +64,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await authRouter.register(registerIdentityAccessTokenRouter);
|
||||
await authRouter.register(registerIdentityAwsAuthRouter);
|
||||
await authRouter.register(registerIdentityAzureAuthRouter);
|
||||
await authRouter.register(registerIdentityOciAuthRouter);
|
||||
await authRouter.register(registerIdentityOidcAuthRouter);
|
||||
await authRouter.register(registerIdentityJwtAuthRouter);
|
||||
await authRouter.register(registerIdentityLdapAuthRouter);
|
||||
@@ -105,6 +108,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await pkiRouter.register(registerCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
||||
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
|
||||
await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" });
|
||||
},
|
||||
{ prefix: "/pki" }
|
||||
);
|
||||
|
@@ -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";
|
||||
|
||||
@@ -490,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",
|
||||
@@ -628,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)
|
||||
}),
|
||||
@@ -666,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
|
||||
};
|
||||
};
|
||||
|
@@ -36,6 +36,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
)
|
||||
.leftJoin(TableName.IdentityOciAuth, `${TableName.Identity}.id`, `${TableName.IdentityOciAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
|
||||
@@ -46,6 +47,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAwsAuth).as("accessTokenTrustedIpsAws"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityAzureAuth).as("accessTokenTrustedIpsAzure"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOciAuth).as("accessTokenTrustedIpsOci"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
|
||||
@@ -63,6 +65,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
trustedIpsAwsAuth: doc.accessTokenTrustedIpsAws,
|
||||
trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure,
|
||||
trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s,
|
||||
trustedIpsOciAuth: doc.accessTokenTrustedIpsOci,
|
||||
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
|
||||
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
|
||||
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt,
|
||||
|
@@ -182,6 +182,7 @@ export const identityAccessTokenServiceFactory = ({
|
||||
[IdentityAuthMethod.UNIVERSAL_AUTH]: identityAccessToken.trustedIpsUniversalAuth,
|
||||
[IdentityAuthMethod.GCP_AUTH]: identityAccessToken.trustedIpsGcpAuth,
|
||||
[IdentityAuthMethod.AWS_AUTH]: identityAccessToken.trustedIpsAwsAuth,
|
||||
[IdentityAuthMethod.OCI_AUTH]: identityAccessToken.trustedIpsOciAuth,
|
||||
[IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth,
|
||||
[IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth,
|
||||
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
|
||||
|
@@ -0,0 +1,9 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TIdentityOciAuthDALFactory = ReturnType<typeof identityOciAuthDALFactory>;
|
||||
|
||||
export const identityOciAuthDALFactory = (db: TDbClient) => {
|
||||
return ormify(db, TableName.IdentityOciAuth);
|
||||
};
|
@@ -0,0 +1,371 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { AxiosError } from "axios";
|
||||
import jwt from "jsonwebtoken";
|
||||
import RE2 from "re2";
|
||||
|
||||
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||
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 { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
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 { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
|
||||
import { TIdentityOciAuthDALFactory } from "./identity-oci-auth-dal";
|
||||
import {
|
||||
TAttachOciAuthDTO,
|
||||
TGetOciAuthDTO,
|
||||
TLoginOciAuthDTO,
|
||||
TOciGetUserResponse,
|
||||
TRevokeOciAuthDTO,
|
||||
TUpdateOciAuthDTO
|
||||
} from "./identity-oci-auth-types";
|
||||
|
||||
type TIdentityOciAuthServiceFactoryDep = {
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
|
||||
identityOciAuthDAL: Pick<TIdentityOciAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
};
|
||||
|
||||
export type TIdentityOciAuthServiceFactory = ReturnType<typeof identityOciAuthServiceFactory>;
|
||||
|
||||
export const identityOciAuthServiceFactory = ({
|
||||
identityAccessTokenDAL,
|
||||
identityOciAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
licenseService,
|
||||
permissionService
|
||||
}: TIdentityOciAuthServiceFactoryDep) => {
|
||||
const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => {
|
||||
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
|
||||
if (!identityOciAuth) {
|
||||
throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" });
|
||||
}
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityOciAuth.identityId });
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(headers.host);
|
||||
|
||||
// Validate OCI host format
|
||||
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid OCI host format. Expected format: identity.<region>.oraclecloud.com"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await request
|
||||
.get<TOciGetUserResponse>(`https://${headers.host}/20160918/users/${userOcid}`, {
|
||||
headers
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
logger.error(err.response, "OciIdentityLogin: Failed to authenticate with Oracle Cloud");
|
||||
throw err;
|
||||
});
|
||||
|
||||
if (data.compartmentId !== identityOciAuth.tenancyOcid) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: OCI account isn't part of tenancy."
|
||||
});
|
||||
}
|
||||
|
||||
if (identityOciAuth.allowedUsernames) {
|
||||
const isAccountAllowed = identityOciAuth.allowedUsernames.split(",").some((name) => name.trim() === data.name);
|
||||
|
||||
if (!isAccountAllowed)
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: OCI account username not allowed."
|
||||
});
|
||||
}
|
||||
|
||||
// Generate the token
|
||||
const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => {
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityOciAuth.identityId,
|
||||
isAccessTokenRevoked: false,
|
||||
accessTokenTTL: identityOciAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit,
|
||||
authMethod: IdentityAuthMethod.OCI_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
return newToken;
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
identityId: identityOciAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
Number(identityAccessToken.accessTokenTTL) === 0
|
||||
? undefined
|
||||
: {
|
||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
identityOciAuth,
|
||||
accessToken,
|
||||
identityAccessToken,
|
||||
identityMembershipOrg
|
||||
};
|
||||
};
|
||||
|
||||
const attachOciAuth = async ({
|
||||
identityId,
|
||||
tenancyOcid,
|
||||
allowedUsernames,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId,
|
||||
isActorSuperAdmin
|
||||
}: TAttachOciAuthDTO) => {
|
||||
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.OCI_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add OCI 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);
|
||||
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);
|
||||
});
|
||||
|
||||
const identityOciAuth = await identityOciAuthDAL.transaction(async (tx) => {
|
||||
const doc = await identityOciAuthDAL.create(
|
||||
{
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
type: "iam",
|
||||
tenancyOcid,
|
||||
allowedUsernames,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityOciAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const updateOciAuth = async ({
|
||||
identityId,
|
||||
tenancyOcid,
|
||||
allowedUsernames,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateOciAuthDTO) => {
|
||||
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.OCI_AUTH)) {
|
||||
throw new NotFoundError({
|
||||
message: "The identity does not have OCI Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
|
||||
|
||||
if (
|
||||
(accessTokenMaxTTL || identityOciAuth.accessTokenMaxTTL) > 0 &&
|
||||
(accessTokenTTL || identityOciAuth.accessTokenTTL) > (accessTokenMaxTTL || identityOciAuth.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);
|
||||
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);
|
||||
});
|
||||
|
||||
const updatedOciAuth = await identityOciAuthDAL.updateById(identityOciAuth.id, {
|
||||
tenancyOcid,
|
||||
allowedUsernames,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
: undefined
|
||||
});
|
||||
|
||||
return { ...updatedOciAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const getOciAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetOciAuthDTO) => {
|
||||
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.OCI_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have OCI Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const ociIdentityAuth = await identityOciAuthDAL.findOne({ identityId });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
return { ...ociIdentityAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const revokeIdentityOciAuth = async ({
|
||||
identityId,
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TRevokeOciAuthDTO) => {
|
||||
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.OCI_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have OCI auth"
|
||||
});
|
||||
}
|
||||
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 OCI auth of identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const revokedIdentityOciAuth = await identityOciAuthDAL.transaction(async (tx) => {
|
||||
const deletedOciAuth = await identityOciAuthDAL.delete({ identityId }, tx);
|
||||
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.OCI_AUTH }, tx);
|
||||
|
||||
return { ...deletedOciAuth?.[0], orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
return revokedIdentityOciAuth;
|
||||
};
|
||||
|
||||
return {
|
||||
login,
|
||||
attachOciAuth,
|
||||
updateOciAuth,
|
||||
getOciAuth,
|
||||
revokeIdentityOciAuth
|
||||
};
|
||||
};
|
@@ -0,0 +1,53 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TLoginOciAuthDTO = {
|
||||
identityId: string;
|
||||
userOcid: string;
|
||||
headers: {
|
||||
authorization: string;
|
||||
host: string;
|
||||
"x-date": string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TAttachOciAuthDTO = {
|
||||
identityId: string;
|
||||
tenancyOcid: string;
|
||||
allowedUsernames: string | null;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: { ipAddress: string }[];
|
||||
isActorSuperAdmin?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateOciAuthDTO = {
|
||||
identityId: string;
|
||||
tenancyOcid: string;
|
||||
allowedUsernames: string | null;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetOciAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRevokeOciAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TOciGetUserResponse = {
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
timeModified: string;
|
||||
isMfaActivated: boolean;
|
||||
id: string;
|
||||
compartmentId: string;
|
||||
name: string;
|
||||
timeCreated: string;
|
||||
freeformTags: { [key: string]: string };
|
||||
lifecycleState: string;
|
||||
};
|
@@ -0,0 +1,32 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
const usernameSchema = z
|
||||
.string()
|
||||
.min(1, "Username cannot be empty")
|
||||
.refine((val) => new RE2("^[a-zA-Z0-9._@-]+$").test(val), "Invalid OCI username format");
|
||||
export const validateUsernames = z
|
||||
.string()
|
||||
.trim()
|
||||
.max(500, "Input exceeds the maximum limit of 500 characters")
|
||||
.nullish()
|
||||
.transform((val) => {
|
||||
if (!val) return [];
|
||||
return val
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
})
|
||||
.refine((arr) => arr.every((name) => usernameSchema.safeParse(name).success), {
|
||||
message: "One or more usernames are invalid"
|
||||
})
|
||||
.transform((arr) => (arr.length > 0 ? arr.join(", ") : null));
|
||||
|
||||
export const validateTenancy = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Tenancy OCID cannot be empty.")
|
||||
.refine(
|
||||
(val) => new RE2("^ocid1\\.tenancy\\.oc1\\..+$").test(val),
|
||||
"Invalid Tenancy OCID format. Must start with ocid1.tenancy.oc1."
|
||||
);
|
@@ -8,6 +8,7 @@ import {
|
||||
TIdentityAzureAuths,
|
||||
TIdentityGcpAuths,
|
||||
TIdentityKubernetesAuths,
|
||||
TIdentityOciAuths,
|
||||
TIdentityOidcAuths,
|
||||
TIdentityTokenAuths,
|
||||
TIdentityUniversalAuths
|
||||
@@ -66,6 +67,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityOciAuth,
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
`${TableName.IdentityOciAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityOidcAuth,
|
||||
`${TableName.IdentityProjectMembership}.identityId`,
|
||||
@@ -107,6 +113,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
|
||||
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
|
||||
@@ -270,6 +277,11 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityOciAuths>(
|
||||
TableName.IdentityOciAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityOciAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityOidcAuths>(
|
||||
TableName.IdentityOidcAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
@@ -309,6 +321,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
|
||||
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth)
|
||||
@@ -336,6 +349,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
ociId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
@@ -356,6 +370,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
ociId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId
|
||||
|
@@ -5,6 +5,7 @@ export const buildAuthMethods = ({
|
||||
gcpId,
|
||||
awsId,
|
||||
kubernetesId,
|
||||
ociId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
@@ -15,6 +16,7 @@ export const buildAuthMethods = ({
|
||||
gcpId?: string;
|
||||
awsId?: string;
|
||||
kubernetesId?: string;
|
||||
ociId?: string;
|
||||
oidcId?: string;
|
||||
azureId?: string;
|
||||
tokenId?: string;
|
||||
@@ -26,6 +28,7 @@ export const buildAuthMethods = ({
|
||||
...[gcpId ? IdentityAuthMethod.GCP_AUTH : null],
|
||||
...[awsId ? IdentityAuthMethod.AWS_AUTH : null],
|
||||
...[kubernetesId ? IdentityAuthMethod.KUBERNETES_AUTH : null],
|
||||
...[ociId ? IdentityAuthMethod.OCI_AUTH : null],
|
||||
...[oidcId ? IdentityAuthMethod.OIDC_AUTH : null],
|
||||
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
|
||||
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
TIdentityGcpAuths,
|
||||
TIdentityJwtAuths,
|
||||
TIdentityKubernetesAuths,
|
||||
TIdentityOciAuths,
|
||||
TIdentityOidcAuths,
|
||||
TIdentityOrgMemberships,
|
||||
TIdentityTokenAuths,
|
||||
@@ -62,6 +63,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityOciAuths>(
|
||||
TableName.IdentityOciAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityOciAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityOidcAuths>(
|
||||
TableName.IdentityOidcAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
@@ -95,6 +101,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
|
||||
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
@@ -186,6 +193,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityOciAuths>(
|
||||
TableName.IdentityOciAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityOciAuth}.identityId`
|
||||
)
|
||||
.leftJoin<TIdentityOidcAuths>(
|
||||
TableName.IdentityOidcAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
@@ -226,6 +238,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
|
||||
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
@@ -269,6 +282,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
gcpId,
|
||||
jwtId,
|
||||
kubernetesId,
|
||||
ociId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
@@ -301,6 +315,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
ociId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
@@ -401,6 +416,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityKubernetesAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityOciAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityOciAuth}.identityId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.IdentityOidcAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
@@ -441,6 +461,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
|
||||
db.ref("id").as("awsId").withSchema(TableName.IdentityAwsAuth),
|
||||
db.ref("id").as("kubernetesId").withSchema(TableName.IdentityKubernetesAuth),
|
||||
db.ref("id").as("ociId").withSchema(TableName.IdentityOciAuth),
|
||||
db.ref("id").as("oidcId").withSchema(TableName.IdentityOidcAuth),
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
@@ -485,6 +506,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
gcpId,
|
||||
jwtId,
|
||||
kubernetesId,
|
||||
ociId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
@@ -517,6 +539,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
awsId,
|
||||
gcpId,
|
||||
kubernetesId,
|
||||
ociId,
|
||||
oidcId,
|
||||
azureId,
|
||||
tokenId,
|
||||
|
@@ -106,18 +106,29 @@ export const identityServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
let insertedMetadata: Array<{
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}> = [];
|
||||
|
||||
if (metadata && metadata.length) {
|
||||
await identityMetadataDAL.insertMany(
|
||||
metadata.map(({ key, value }) => ({
|
||||
identityId: newIdentity.id,
|
||||
orgId,
|
||||
key,
|
||||
value
|
||||
})),
|
||||
tx
|
||||
);
|
||||
const rowsToInsert = metadata.map(({ key, value }) => ({
|
||||
identityId: newIdentity.id,
|
||||
orgId,
|
||||
key,
|
||||
value
|
||||
}));
|
||||
|
||||
insertedMetadata = await identityMetadataDAL.insertMany(rowsToInsert, tx);
|
||||
}
|
||||
return { ...newIdentity, authMethods: [] };
|
||||
|
||||
return {
|
||||
...newIdentity,
|
||||
authMethods: [],
|
||||
metadata: insertedMetadata
|
||||
};
|
||||
});
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
|
||||
@@ -189,21 +200,31 @@ export const identityServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
}
|
||||
let insertedMetadata: Array<{
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}> = [];
|
||||
|
||||
if (metadata) {
|
||||
await identityMetadataDAL.delete({ orgId: identityOrgMembership.orgId, identityId: id }, tx);
|
||||
|
||||
if (metadata.length) {
|
||||
await identityMetadataDAL.insertMany(
|
||||
metadata.map(({ key, value }) => ({
|
||||
identityId: newIdentity.id,
|
||||
orgId: identityOrgMembership.orgId,
|
||||
key,
|
||||
value
|
||||
})),
|
||||
tx
|
||||
);
|
||||
const rowsToInsert = metadata.map(({ key, value }) => ({
|
||||
identityId: newIdentity.id,
|
||||
orgId: identityOrgMembership.orgId,
|
||||
key,
|
||||
value
|
||||
}));
|
||||
|
||||
insertedMetadata = await identityMetadataDAL.insertMany(rowsToInsert, tx);
|
||||
}
|
||||
}
|
||||
return newIdentity;
|
||||
|
||||
return {
|
||||
...newIdentity,
|
||||
metadata: insertedMetadata
|
||||
};
|
||||
});
|
||||
|
||||
return { ...identity, orgId: identityOrgMembership.orgId };
|
||||
@@ -224,6 +245,7 @@ export const identityServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
return identity;
|
||||
};
|
||||
|
||||
|
@@ -177,7 +177,6 @@ export const deleteGithubSecrets = async ({
|
||||
selected_repositories_url?: string | undefined;
|
||||
}
|
||||
|
||||
// @ts-expect-error just octokit ts compatiability issue
|
||||
const OctokitWithRetry = Octokit.plugin(retry);
|
||||
let octokit: Octokit;
|
||||
const appCfg = getConfig();
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
Request,
|
||||
Response
|
||||
} from "botbuilder";
|
||||
import { CronJob } from "cron";
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
@@ -86,8 +87,17 @@ export const microsoftTeamsServiceFactory = ({
|
||||
}: TMicrosoftTeamsServiceFactoryDep) => {
|
||||
let teamsBot: TeamsBot | null = null;
|
||||
let adapter: CloudAdapter | null = null;
|
||||
let lastKnownUpdatedAt = new Date();
|
||||
|
||||
const initializeTeamsBot = async ({ botAppId, botAppPassword }: { botAppId: string; botAppPassword: string }) => {
|
||||
const initializeTeamsBot = async ({
|
||||
botAppId,
|
||||
botAppPassword,
|
||||
lastUpdatedAt
|
||||
}: {
|
||||
botAppId: string;
|
||||
botAppPassword: string;
|
||||
lastUpdatedAt?: Date;
|
||||
}) => {
|
||||
logger.info("Initializing Microsoft Teams bot");
|
||||
teamsBot = new TeamsBot({
|
||||
botAppId,
|
||||
@@ -106,6 +116,57 @@ export const microsoftTeamsServiceFactory = ({
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (lastUpdatedAt) {
|
||||
lastKnownUpdatedAt = lastUpdatedAt;
|
||||
}
|
||||
};
|
||||
|
||||
const $syncMicrosoftTeamsIntegrationConfiguration = async () => {
|
||||
try {
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
if (!serverCfg) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to get server configuration."
|
||||
});
|
||||
}
|
||||
|
||||
if (lastKnownUpdatedAt.getTime() === serverCfg.updatedAt.getTime()) {
|
||||
logger.info("No changes to Microsoft Teams integration configuration, skipping sync");
|
||||
return;
|
||||
}
|
||||
|
||||
lastKnownUpdatedAt = serverCfg.updatedAt;
|
||||
|
||||
if (
|
||||
serverCfg.encryptedMicrosoftTeamsAppId &&
|
||||
serverCfg.encryptedMicrosoftTeamsClientSecret &&
|
||||
serverCfg.encryptedMicrosoftTeamsBotId
|
||||
) {
|
||||
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||
const decryptedAppId = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsAppId);
|
||||
const decryptedAppPassword = decryptWithRoot(serverCfg.encryptedMicrosoftTeamsClientSecret);
|
||||
|
||||
await initializeTeamsBot({
|
||||
botAppId: decryptedAppId.toString(),
|
||||
botAppPassword: decryptedAppPassword.toString()
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err, "Error syncing Microsoft Teams integration configuration");
|
||||
}
|
||||
};
|
||||
|
||||
const initializeBackgroundSync = async () => {
|
||||
logger.info("Setting up background sync process for Microsoft Teams workflow integration configuration");
|
||||
// initial sync upon startup
|
||||
await $syncMicrosoftTeamsIntegrationConfiguration();
|
||||
|
||||
// sync rate limits configuration every 5 minutes
|
||||
const job = new CronJob("*/5 * * * *", $syncMicrosoftTeamsIntegrationConfiguration);
|
||||
job.start();
|
||||
|
||||
return job;
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
@@ -703,6 +764,7 @@ export const microsoftTeamsServiceFactory = ({
|
||||
getTeams,
|
||||
handleMessageEndpoint,
|
||||
start,
|
||||
initializeBackgroundSync,
|
||||
sendNotification,
|
||||
checkInstallationStatus,
|
||||
getClientId
|
||||
|
10
backend/src/services/pki-subscriber/pki-subscriber-dal.ts
Normal file
10
backend/src/services/pki-subscriber/pki-subscriber-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TPkiSubscriberDALFactory = ReturnType<typeof pkiSubscriberDALFactory>;
|
||||
|
||||
export const pkiSubscriberDALFactory = (db: TDbClient) => {
|
||||
const pkiSubscriberOrm = ormify(db, TableName.PkiSubscriber);
|
||||
return pkiSubscriberOrm;
|
||||
};
|
14
backend/src/services/pki-subscriber/pki-subscriber-schema.ts
Normal file
14
backend/src/services/pki-subscriber/pki-subscriber-schema.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { PkiSubscribersSchema } from "@app/db/schemas";
|
||||
|
||||
export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({
|
||||
id: true,
|
||||
projectId: true,
|
||||
caId: true,
|
||||
name: true,
|
||||
commonName: true,
|
||||
status: true,
|
||||
subjectAlternativeNames: true,
|
||||
ttl: true,
|
||||
keyUsages: true,
|
||||
extendedKeyUsages: true
|
||||
});
|
805
backend/src/services/pki-subscriber/pki-subscriber-service.ts
Normal file
805
backend/src/services/pki-subscriber/pki-subscriber-service.ts
Normal file
@@ -0,0 +1,805 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import crypto, { KeyObject } from "crypto";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertExtendedKeyUsageOIDToName,
|
||||
CertKeyAlgorithm,
|
||||
CertKeyUsage,
|
||||
CertStatus
|
||||
} from "@app/services/certificate/certificate-types";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import {
|
||||
createSerialNumber,
|
||||
getCaCertChain,
|
||||
getCaCredentials,
|
||||
keyAlgorithmToAlgCfg,
|
||||
parseDistinguishedName
|
||||
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
import {
|
||||
PkiSubscriberStatus,
|
||||
TCreatePkiSubscriberDTO,
|
||||
TDeletePkiSubscriberDTO,
|
||||
TGetPkiSubscriberDTO,
|
||||
TIssuePkiSubscriberCertDTO,
|
||||
TListPkiSubscriberCertsDTO,
|
||||
TSignPkiSubscriberCertDTO,
|
||||
TUpdatePkiSubscriberDTO
|
||||
} from "./pki-subscriber-types";
|
||||
|
||||
type TPkiSubscriberServiceFactoryDep = {
|
||||
pkiSubscriberDAL: Pick<
|
||||
TPkiSubscriberDALFactory,
|
||||
"create" | "findById" | "updateById" | "deleteById" | "transaction" | "find" | "findOne"
|
||||
>;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "countCertificatesForPkiSubscriber" | "find">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById" | "find">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "decryptWithKmsKey" | "encryptWithKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
|
||||
export type TPkiSubscriberServiceFactory = ReturnType<typeof pkiSubscriberServiceFactory>;
|
||||
|
||||
export const pkiSubscriberServiceFactory = ({
|
||||
pkiSubscriberDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
}: TPkiSubscriberServiceFactoryDep) => {
|
||||
const createSubscriber = async ({
|
||||
name,
|
||||
commonName,
|
||||
status,
|
||||
caId,
|
||||
ttl,
|
||||
subjectAlternativeNames,
|
||||
keyUsages,
|
||||
extendedKeyUsages,
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreatePkiSubscriberDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSubscriberActions.Create,
|
||||
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||
name
|
||||
})
|
||||
);
|
||||
|
||||
const newSubscriber = await pkiSubscriberDAL.create({
|
||||
caId,
|
||||
projectId,
|
||||
name,
|
||||
commonName,
|
||||
status,
|
||||
ttl,
|
||||
subjectAlternativeNames,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
});
|
||||
|
||||
return newSubscriber;
|
||||
};
|
||||
|
||||
const getSubscriber = async ({
|
||||
subscriberName,
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TGetPkiSubscriberDTO) => {
|
||||
const subscriber = await pkiSubscriberDAL.findOne({
|
||||
name: subscriberName,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: subscriber.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSubscriberActions.Read,
|
||||
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||
name: subscriber.name
|
||||
})
|
||||
);
|
||||
|
||||
return subscriber;
|
||||
};
|
||||
|
||||
const updateSubscriber = async ({
|
||||
subscriberName,
|
||||
projectId,
|
||||
name,
|
||||
commonName,
|
||||
status,
|
||||
caId,
|
||||
ttl,
|
||||
subjectAlternativeNames,
|
||||
keyUsages,
|
||||
extendedKeyUsages,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdatePkiSubscriberDTO) => {
|
||||
const subscriber = await pkiSubscriberDAL.findOne({
|
||||
name: subscriberName,
|
||||
projectId
|
||||
});
|
||||
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: subscriber.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSubscriberActions.Edit,
|
||||
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||
name: subscriber.name
|
||||
})
|
||||
);
|
||||
|
||||
const updatedSubscriber = await pkiSubscriberDAL.updateById(subscriber.id, {
|
||||
caId,
|
||||
name,
|
||||
commonName,
|
||||
status,
|
||||
ttl,
|
||||
subjectAlternativeNames,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
});
|
||||
|
||||
return updatedSubscriber;
|
||||
};
|
||||
|
||||
const deleteSubscriber = async ({
|
||||
subscriberName,
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TDeletePkiSubscriberDTO) => {
|
||||
const subscriber = await pkiSubscriberDAL.findOne({
|
||||
name: subscriberName,
|
||||
projectId
|
||||
});
|
||||
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: subscriber.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSubscriberActions.Delete,
|
||||
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||
name: subscriber.name
|
||||
})
|
||||
);
|
||||
|
||||
await pkiSubscriberDAL.deleteById(subscriber.id);
|
||||
|
||||
return subscriber;
|
||||
};
|
||||
|
||||
const issueSubscriberCert = async ({
|
||||
subscriberName,
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TIssuePkiSubscriberCertDTO) => {
|
||||
const subscriber = await pkiSubscriberDAL.findOne({
|
||||
name: subscriberName,
|
||||
projectId
|
||||
});
|
||||
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
|
||||
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(subscriber.caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSubscriberActions.IssueCert,
|
||||
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||
name: subscriber.name
|
||||
})
|
||||
);
|
||||
|
||||
if (subscriber.status !== PkiSubscriberStatus.ACTIVE)
|
||||
throw new BadRequestError({ message: "Subscriber is not active" });
|
||||
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) {
|
||||
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
||||
}
|
||||
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
|
||||
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||
const notBeforeDate = new Date();
|
||||
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl));
|
||||
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||
|
||||
// check not before constraint
|
||||
if (notBeforeDate < caCertNotBeforeDate) {
|
||||
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||
}
|
||||
|
||||
// check not after constraint
|
||||
if (notAfterDate > caCertNotAfterDate) {
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||
const leafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
|
||||
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||
name: `CN=${subscriber.commonName}`,
|
||||
keys: leafKeys,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
// eslint-disable-next-line no-bitwise
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment)
|
||||
],
|
||||
attributes: [new x509.ChallengePasswordAttribute("password")]
|
||||
});
|
||||
|
||||
const { caPrivateKey, caSecret } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const appCfg = getConfig();
|
||||
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
|
||||
new x509.AuthorityInfoAccessExtension({
|
||||
caIssuers: new x509.GeneralName("url", caIssuerUrl)
|
||||
}),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
|
||||
];
|
||||
|
||||
const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[];
|
||||
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
|
||||
if (keyUsagesBitValue) {
|
||||
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
|
||||
}
|
||||
|
||||
if (subscriber.extendedKeyUsages.length) {
|
||||
const extendedKeyUsagesExtension = new x509.ExtendedKeyUsageExtension(
|
||||
subscriber.extendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku as CertExtendedKeyUsage]),
|
||||
true
|
||||
);
|
||||
extensions.push(extendedKeyUsagesExtension);
|
||||
}
|
||||
|
||||
let altNamesArray: { type: "email" | "dns"; value: string }[] = [];
|
||||
|
||||
if (subscriber.subjectAlternativeNames?.length) {
|
||||
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
|
||||
if (z.string().email().safeParse(altName).success) {
|
||||
return { type: "email", value: altName };
|
||||
}
|
||||
|
||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||
return { type: "dns", value: altName };
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
|
||||
});
|
||||
|
||||
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||
extensions.push(altNamesExtension);
|
||||
}
|
||||
|
||||
const serialNumber = createSerialNumber();
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: csrObj.subject,
|
||||
issuer: caCertObj.subject,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
signingKey: caPrivateKey,
|
||||
publicKey: csrObj.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions
|
||||
});
|
||||
|
||||
const skLeafObj = KeyObject.from(leafKeys.privateKey);
|
||||
const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string;
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||
});
|
||||
const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({
|
||||
plainText: Buffer.from(skLeaf)
|
||||
});
|
||||
|
||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||
caCertId: caCert.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(certificateChainPem)
|
||||
});
|
||||
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
caCertId: caCert.id,
|
||||
pkiSubscriberId: subscriber.id,
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: subscriber.commonName,
|
||||
commonName: subscriber.commonName,
|
||||
altNames: subscriber.subjectAlternativeNames.join(","),
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
keyUsages: selectedKeyUsages,
|
||||
extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[]
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateBodyDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateSecretDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedPrivateKey
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: leafCert.toString("pem"),
|
||||
certificateChain: certificateChainPem,
|
||||
issuingCaCertificate,
|
||||
privateKey: skLeaf,
|
||||
serialNumber,
|
||||
ca,
|
||||
subscriber
|
||||
};
|
||||
};
|
||||
|
||||
const signSubscriberCert = async ({
|
||||
subscriberName,
|
||||
projectId,
|
||||
csr,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TSignPkiSubscriberCertDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const subscriber = await pkiSubscriberDAL.findOne({
|
||||
name: subscriberName,
|
||||
projectId
|
||||
});
|
||||
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
|
||||
if (!subscriber.caId) throw new BadRequestError({ message: "Subscriber does not have an assigned issuing CA" });
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(subscriber.caId);
|
||||
if (!ca) throw new NotFoundError({ message: `CA with ID '${subscriber.caId}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSubscriberActions.IssueCert,
|
||||
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||
name: subscriber.name
|
||||
})
|
||||
);
|
||||
|
||||
if (subscriber.status !== PkiSubscriberStatus.ACTIVE)
|
||||
throw new BadRequestError({ message: "Subscriber is not active" });
|
||||
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) {
|
||||
throw new BadRequestError({ message: "Certificate template is required for issuance" });
|
||||
}
|
||||
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: ca.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const decryptedCaCert = await kmsDecryptor({
|
||||
cipherTextBlob: caCert.encryptedCertificate
|
||||
});
|
||||
|
||||
const caCertObj = new x509.X509Certificate(decryptedCaCert);
|
||||
const notBeforeDate = new Date();
|
||||
const notAfterDate = new Date(new Date().getTime() + ms(subscriber.ttl));
|
||||
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||
const caCertNotAfterDate = new Date(caCertObj.notAfter);
|
||||
|
||||
// check not before constraint
|
||||
if (notBeforeDate < caCertNotBeforeDate) {
|
||||
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
|
||||
}
|
||||
|
||||
// check not after constraint
|
||||
if (notAfterDate > caCertNotAfterDate) {
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
|
||||
|
||||
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||
|
||||
const dn = parseDistinguishedName(csrObj.subject);
|
||||
const cn = dn.commonName;
|
||||
if (cn !== subscriber.commonName) {
|
||||
throw new BadRequestError({ message: "Common name (CN) in the CSR does not match the subscriber's common name" });
|
||||
}
|
||||
|
||||
const { caPrivateKey, caSecret } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
|
||||
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||
new x509.AuthorityInfoAccessExtension({
|
||||
caIssuers: new x509.GeneralName("url", caIssuerUrl)
|
||||
}),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
|
||||
];
|
||||
|
||||
// handle key usages
|
||||
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
|
||||
let csrKeyUsages: CertKeyUsage[] = [];
|
||||
if (csrKeyUsageExtension) {
|
||||
csrKeyUsages = Object.values(CertKeyUsage).filter(
|
||||
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
|
||||
);
|
||||
}
|
||||
|
||||
const selectedKeyUsages = subscriber.keyUsages as CertKeyUsage[];
|
||||
|
||||
if (csrKeyUsages.some((keyUsage) => !selectedKeyUsages.includes(keyUsage))) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid key usage value based on subscriber's specified key usages"
|
||||
});
|
||||
}
|
||||
|
||||
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
|
||||
if (keyUsagesBitValue) {
|
||||
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
|
||||
}
|
||||
|
||||
// handle extended key usages
|
||||
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
|
||||
let csrExtendedKeyUsages: CertExtendedKeyUsage[] = [];
|
||||
if (csrExtendedKeyUsageExtension) {
|
||||
csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
|
||||
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
|
||||
);
|
||||
}
|
||||
|
||||
const selectedExtendedKeyUsages = subscriber.extendedKeyUsages as CertExtendedKeyUsage[];
|
||||
if (csrExtendedKeyUsages.some((eku) => !selectedExtendedKeyUsages.includes(eku))) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid extended key usage value based on subscriber's specified extended key usages"
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedExtendedKeyUsages.length) {
|
||||
extensions.push(
|
||||
new x509.ExtendedKeyUsageExtension(
|
||||
selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// attempt to read from CSR if altNames is not explicitly provided
|
||||
let altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
}[] = [];
|
||||
|
||||
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||
if (sanExtension) {
|
||||
const sanNames = new x509.GeneralNames(sanExtension.value);
|
||||
|
||||
altNamesArray = sanNames.items
|
||||
.filter((value) => value.type === "email" || value.type === "dns")
|
||||
.map((name) => ({
|
||||
type: name.type as "email" | "dns",
|
||||
value: name.value
|
||||
}));
|
||||
}
|
||||
|
||||
if (
|
||||
altNamesArray
|
||||
.map((altName) => altName.value)
|
||||
.some((altName) => !subscriber.subjectAlternativeNames.includes(altName))
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid subject alternative name based on subscriber's specified subject alternative names"
|
||||
});
|
||||
}
|
||||
|
||||
if (altNamesArray.length) {
|
||||
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||
extensions.push(altNamesExtension);
|
||||
}
|
||||
|
||||
const serialNumber = createSerialNumber();
|
||||
const leafCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: csrObj.subject,
|
||||
issuer: caCertObj.subject,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
signingKey: caPrivateKey,
|
||||
publicKey: csrObj.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions
|
||||
});
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
|
||||
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
|
||||
});
|
||||
|
||||
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
|
||||
caCertId: ca.activeCaCertId,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificateChainPem = `${issuingCaCertificate}\n${caCertChain}`.trim();
|
||||
|
||||
const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(certificateChainPem)
|
||||
});
|
||||
|
||||
await certificateDAL.transaction(async (tx) => {
|
||||
const cert = await certificateDAL.create(
|
||||
{
|
||||
caId: ca.id,
|
||||
caCertId: caCert.id,
|
||||
pkiSubscriberId: subscriber.id,
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: subscriber.commonName,
|
||||
commonName: subscriber.commonName,
|
||||
altNames: subscriber.subjectAlternativeNames.join(","),
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate,
|
||||
keyUsages: selectedKeyUsages,
|
||||
extendedKeyUsages: selectedExtendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await certificateBodyDAL.create(
|
||||
{
|
||||
certId: cert.id,
|
||||
encryptedCertificate,
|
||||
encryptedCertificateChain
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return cert;
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: leafCert.toString("pem"),
|
||||
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||
issuingCaCertificate,
|
||||
serialNumber,
|
||||
ca,
|
||||
commonName: subscriber.commonName,
|
||||
subscriber
|
||||
};
|
||||
};
|
||||
|
||||
const listSubscriberCerts = async ({
|
||||
subscriberName,
|
||||
projectId,
|
||||
offset,
|
||||
limit,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TListPkiSubscriberCertsDTO) => {
|
||||
const subscriber = await pkiSubscriberDAL.findOne({
|
||||
name: subscriberName,
|
||||
projectId
|
||||
});
|
||||
if (!subscriber) throw new NotFoundError({ message: `PKI subscriber named '${subscriberName}' not found` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: subscriber.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionPkiSubscriberActions.ListCerts,
|
||||
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||
name: subscriber.name
|
||||
})
|
||||
);
|
||||
|
||||
const certificates = await certificateDAL.find(
|
||||
{
|
||||
pkiSubscriberId: subscriber.id
|
||||
},
|
||||
{ offset, limit, sort: [["updatedAt", "desc"]] }
|
||||
);
|
||||
|
||||
const count = await certificateDAL.countCertificatesForPkiSubscriber(subscriber.id);
|
||||
|
||||
return {
|
||||
certificates,
|
||||
totalCount: count
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createSubscriber,
|
||||
getSubscriber,
|
||||
updateSubscriber,
|
||||
deleteSubscriber,
|
||||
issueSubscriberCert,
|
||||
signSubscriberCert,
|
||||
listSubscriberCerts
|
||||
};
|
||||
};
|
54
backend/src/services/pki-subscriber/pki-subscriber-types.ts
Normal file
54
backend/src/services/pki-subscriber/pki-subscriber-types.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificate/certificate-types";
|
||||
|
||||
export enum PkiSubscriberStatus {
|
||||
ACTIVE = "active",
|
||||
DISABLED = "disabled"
|
||||
}
|
||||
|
||||
export type TCreatePkiSubscriberDTO = {
|
||||
caId: string;
|
||||
name: string;
|
||||
commonName: string;
|
||||
status: PkiSubscriberStatus;
|
||||
ttl: string;
|
||||
subjectAlternativeNames: string[];
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetPkiSubscriberDTO = {
|
||||
subscriberName: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdatePkiSubscriberDTO = {
|
||||
subscriberName: string;
|
||||
caId?: string;
|
||||
name?: string;
|
||||
commonName?: string;
|
||||
status?: PkiSubscriberStatus;
|
||||
ttl?: string;
|
||||
subjectAlternativeNames?: string[];
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TDeletePkiSubscriberDTO = {
|
||||
subscriberName: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TIssuePkiSubscriberCertDTO = {
|
||||
subscriberName: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TSignPkiSubscriberCertDTO = {
|
||||
subscriberName: string;
|
||||
csr: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TListPkiSubscriberCertsDTO = {
|
||||
subscriberName: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
} & TProjectPermission;
|
@@ -15,6 +15,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSshHostActions,
|
||||
ProjectPermissionSub
|
||||
@@ -35,6 +36,7 @@ import { groupBy } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { TQueueServiceFactory } from "@app/queue";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||
@@ -86,6 +88,7 @@ import {
|
||||
TListProjectCasDTO,
|
||||
TListProjectCertificateTemplatesDTO,
|
||||
TListProjectCertsDTO,
|
||||
TListProjectPkiSubscribersDTO,
|
||||
TListProjectsDTO,
|
||||
TListProjectSshCasDTO,
|
||||
TListProjectSshCertificatesDTO,
|
||||
@@ -145,6 +148,7 @@ type TProjectServiceFactoryDep = {
|
||||
"findById" | "findByIdWithWorkflowIntegrationDetails"
|
||||
>;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "find">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "find">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "find" | "countCertificatesInProject">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||
@@ -207,6 +211,7 @@ export const projectServiceFactory = ({
|
||||
certificateTemplateDAL,
|
||||
pkiCollectionDAL,
|
||||
pkiAlertDAL,
|
||||
pkiSubscriberDAL,
|
||||
sshCertificateAuthorityDAL,
|
||||
sshCertificateAuthoritySecretDAL,
|
||||
sshCertificateDAL,
|
||||
@@ -1057,6 +1062,45 @@ export const projectServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of PKI subscribers for project
|
||||
*/
|
||||
const listProjectPkiSubscribers = async ({
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
projectId
|
||||
}: TListProjectPkiSubscribersDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
const allowedSubscribers = [];
|
||||
|
||||
// (dangtony98): room to optimize
|
||||
const subscribers = await pkiSubscriberDAL.find({ projectId });
|
||||
|
||||
for (const subscriber of subscribers) {
|
||||
const canRead = permission.can(
|
||||
ProjectPermissionPkiSubscriberActions.Read,
|
||||
subject(ProjectPermissionSub.PkiSubscribers, {
|
||||
name: subscriber.name
|
||||
})
|
||||
);
|
||||
if (canRead) {
|
||||
allowedSubscribers.push(subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
return allowedSubscribers;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of certificate templates for project
|
||||
*/
|
||||
@@ -1156,17 +1200,15 @@ export const projectServiceFactory = ({
|
||||
const hosts = await sshHostDAL.findSshHostsWithLoginMappings(projectId);
|
||||
|
||||
for (const host of hosts) {
|
||||
try {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSshHostActions.Read,
|
||||
subject(ProjectPermissionSub.SshHosts, {
|
||||
hostname: host.hostname
|
||||
})
|
||||
);
|
||||
const canRead = permission.can(
|
||||
ProjectPermissionSshHostActions.Read,
|
||||
subject(ProjectPermissionSub.SshHosts, {
|
||||
hostname: host.hostname
|
||||
})
|
||||
);
|
||||
|
||||
if (canRead) {
|
||||
allowedHosts.push(host);
|
||||
} catch {
|
||||
// intentionally ignore projects where user lacks access
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1930,6 +1972,7 @@ export const projectServiceFactory = ({
|
||||
listProjectSshCas,
|
||||
listProjectSshHosts,
|
||||
listProjectSshHostGroups,
|
||||
listProjectPkiSubscribers,
|
||||
listProjectSshCertificates,
|
||||
listProjectSshCertificateTemplates,
|
||||
updateVersionLimit,
|
||||
|
@@ -155,6 +155,7 @@ export type TListProjectCertificateTemplatesDTO = TProjectPermission;
|
||||
export type TListProjectSshCasDTO = TProjectPermission;
|
||||
export type TListProjectSshHostsDTO = TProjectPermission;
|
||||
export type TListProjectSshCertificateTemplatesDTO = TProjectPermission;
|
||||
export type TListProjectPkiSubscribersDTO = TProjectPermission;
|
||||
export type TListProjectSshCertificatesDTO = {
|
||||
offset: number;
|
||||
limit: number;
|
||||
|
4
backend/src/services/secret-sync/oci-vault/index.ts
Normal file
4
backend/src/services/secret-sync/oci-vault/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./oci-vault-sync-constants";
|
||||
export * from "./oci-vault-sync-fns";
|
||||
export * from "./oci-vault-sync-schemas";
|
||||
export * from "./oci-vault-sync-types";
|
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const OCI_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "OCI Vault",
|
||||
destination: SecretSync.OCIVault,
|
||||
connection: AppConnection.OCI,
|
||||
canImportSecrets: true
|
||||
};
|
292
backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts
Normal file
292
backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { secrets, vault } from "oci-sdk";
|
||||
|
||||
import { delay } from "@app/lib/delay";
|
||||
import { getOCIProvider } from "@app/services/app-connection/oci";
|
||||
import {
|
||||
TCreateOCIVaultVariable,
|
||||
TDeleteOCIVaultVariable,
|
||||
TOCIVaultListVariables,
|
||||
TOCIVaultSyncWithCredentials,
|
||||
TUnmarkOCIVaultVariableFromDeletion,
|
||||
TUpdateOCIVaultVariable
|
||||
} from "@app/services/secret-sync/oci-vault/oci-vault-sync-types";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const listOCIVaultVariables = async ({ provider, compartmentId, vaultId, onlyActive }: TOCIVaultListVariables) => {
|
||||
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
|
||||
const secretsClient = new secrets.SecretsClient({ authenticationDetailsProvider: provider });
|
||||
|
||||
const secretsRes = await vaultsClient.listSecrets({
|
||||
compartmentId,
|
||||
vaultId,
|
||||
lifecycleState: onlyActive ? vault.models.SecretSummary.LifecycleState.Active : undefined
|
||||
});
|
||||
|
||||
const result: Record<string, vault.models.SecretSummary & { name: string; value: string }> = {};
|
||||
|
||||
for await (const s of secretsRes.items) {
|
||||
let secretValue = "";
|
||||
|
||||
if (s.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) {
|
||||
const secretBundle = await secretsClient.getSecretBundle({
|
||||
secretId: s.id
|
||||
});
|
||||
|
||||
secretValue = Buffer.from(secretBundle.secretBundle.secretBundleContent?.content || "", "base64").toString(
|
||||
"utf-8"
|
||||
);
|
||||
}
|
||||
|
||||
result[s.secretName] = {
|
||||
...s,
|
||||
name: s.secretName,
|
||||
value: secretValue
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const createOCIVaultVariable = async ({
|
||||
provider,
|
||||
compartmentId,
|
||||
vaultId,
|
||||
keyId,
|
||||
name,
|
||||
value
|
||||
}: TCreateOCIVaultVariable) => {
|
||||
if (!value) return;
|
||||
|
||||
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
|
||||
|
||||
return vaultsClient.createSecret({
|
||||
createSecretDetails: {
|
||||
compartmentId,
|
||||
vaultId,
|
||||
keyId,
|
||||
secretName: name,
|
||||
enableAutoGeneration: false,
|
||||
secretContent: {
|
||||
content: Buffer.from(value).toString("base64"),
|
||||
contentType: "BASE64"
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateOCIVaultVariable = async ({ provider, secretId, value }: TUpdateOCIVaultVariable) => {
|
||||
if (!value) return;
|
||||
|
||||
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
|
||||
|
||||
return vaultsClient.updateSecret({
|
||||
secretId,
|
||||
updateSecretDetails: {
|
||||
enableAutoGeneration: false,
|
||||
secretContent: {
|
||||
content: Buffer.from(value).toString("base64"),
|
||||
contentType: "BASE64"
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteOCIVaultVariable = async ({ provider, secretId }: TDeleteOCIVaultVariable) => {
|
||||
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
|
||||
|
||||
// Schedule a secret deletion 7 days from now. OCI Vault requires a MINIMUM buffer period of 7 days
|
||||
return vaultsClient.scheduleSecretDeletion({
|
||||
secretId,
|
||||
scheduleSecretDeletionDetails: {
|
||||
timeOfDeletion: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const unmarkOCIVaultVariableFromDeletion = async ({ provider, secretId }: TUnmarkOCIVaultVariableFromDeletion) => {
|
||||
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
|
||||
|
||||
return vaultsClient.cancelSecretDeletion({
|
||||
secretId
|
||||
});
|
||||
};
|
||||
|
||||
export const OCIVaultSyncFns = {
|
||||
syncSecrets: async (secretSync: TOCIVaultSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { compartmentOcid, vaultOcid, keyOcid }
|
||||
} = secretSync;
|
||||
|
||||
const provider = await getOCIProvider(connection);
|
||||
const variables = await listOCIVaultVariables({ provider, compartmentId: compartmentOcid, vaultId: vaultOcid });
|
||||
|
||||
// Throw an error if any keys are updating in OCI vault to prevent skipped updates
|
||||
if (
|
||||
Object.entries(variables).some(
|
||||
([, secret]) =>
|
||||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Updating ||
|
||||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.CancellingDeletion ||
|
||||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Creating ||
|
||||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Deleting ||
|
||||
secret.lifecycleState === vault.models.SecretSummary.LifecycleState.SchedulingDeletion
|
||||
)
|
||||
) {
|
||||
throw new SecretSyncError({
|
||||
error: "Cannot sync while keys are updating in OCI Vault."
|
||||
});
|
||||
}
|
||||
|
||||
// Create secrets
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value }] = entry;
|
||||
|
||||
// skip secrets that don't have a value set
|
||||
if (!value) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingVariable = Object.values(variables).find((v) => v.secretName === key);
|
||||
|
||||
if (!existingVariable) {
|
||||
try {
|
||||
await createOCIVaultVariable({
|
||||
compartmentId: compartmentOcid,
|
||||
vaultId: vaultOcid,
|
||||
provider,
|
||||
keyId: keyOcid,
|
||||
name: key,
|
||||
value
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
} else if (existingVariable.lifecycleState === vault.models.SecretSummary.LifecycleState.PendingDeletion) {
|
||||
// If a secret exists but is pending deletion, cancel the deletion and update the secret
|
||||
await unmarkOCIVaultVariableFromDeletion({
|
||||
provider,
|
||||
compartmentId: compartmentOcid,
|
||||
vaultId: vaultOcid,
|
||||
secretId: existingVariable.id
|
||||
});
|
||||
|
||||
const vaultsClient = new vault.VaultsClient({ authenticationDetailsProvider: provider });
|
||||
const MAX_RETRIES = 10;
|
||||
|
||||
for (let i = 0; i < MAX_RETRIES; i += 1) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delay(5000);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const secret = await vaultsClient.getSecret({
|
||||
secretId: existingVariable.id
|
||||
});
|
||||
|
||||
if (secret.secret.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await updateOCIVaultVariable({
|
||||
provider,
|
||||
compartmentId: compartmentOcid,
|
||||
vaultId: vaultOcid,
|
||||
secretId: existingVariable.id,
|
||||
value
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (i === MAX_RETRIES - 1) {
|
||||
throw new SecretSyncError({
|
||||
error: "Failed to update secret after cancelling deletion.",
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update and delete secrets
|
||||
for await (const [key, variable] of Object.entries(variables)) {
|
||||
// Only update / delete active secrets
|
||||
if (variable.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) {
|
||||
if (key in secretMap && secretMap[key].value.length > 0) {
|
||||
if (variable.value !== secretMap[key].value) {
|
||||
try {
|
||||
await updateOCIVaultVariable({
|
||||
compartmentId: compartmentOcid,
|
||||
vaultId: vaultOcid,
|
||||
provider,
|
||||
secretId: variable.id,
|
||||
value: secretMap[key].value
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (!secretSync.syncOptions.disableSecretDeletion) {
|
||||
try {
|
||||
await deleteOCIVaultVariable({
|
||||
compartmentId: compartmentOcid,
|
||||
vaultId: vaultOcid,
|
||||
provider,
|
||||
secretId: variable.id
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
removeSecrets: async (secretSync: TOCIVaultSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { compartmentOcid, vaultOcid }
|
||||
} = secretSync;
|
||||
|
||||
const provider = await getOCIProvider(connection);
|
||||
const variables = await listOCIVaultVariables({
|
||||
provider,
|
||||
compartmentId: compartmentOcid,
|
||||
vaultId: vaultOcid,
|
||||
onlyActive: true
|
||||
});
|
||||
|
||||
for await (const [key, variable] of Object.entries(variables)) {
|
||||
if (key in secretMap) {
|
||||
try {
|
||||
await deleteOCIVaultVariable({
|
||||
compartmentId: compartmentOcid,
|
||||
vaultId: vaultOcid,
|
||||
provider,
|
||||
secretId: variable.id
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getSecrets: async (secretSync: TOCIVaultSyncWithCredentials) => {
|
||||
const {
|
||||
connection,
|
||||
destinationConfig: { compartmentOcid, vaultOcid }
|
||||
} = secretSync;
|
||||
|
||||
const provider = await getOCIProvider(connection);
|
||||
return listOCIVaultVariables({ provider, compartmentId: compartmentOcid, vaultId: vaultOcid, onlyActive: true });
|
||||
}
|
||||
};
|
@@ -0,0 +1,70 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const OCIVaultSyncDestinationConfigSchema = z.object({
|
||||
compartmentOcid: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Compartment OCID required")
|
||||
.refine(
|
||||
(val) => new RE2("^ocid1\\.(tenancy|compartment)\\.oc1\\..+$").test(val),
|
||||
"Invalid Compartment OCID format. Must start with ocid1.tenancy.oc1. or ocid1.compartment.oc1."
|
||||
)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.compartmentOcid),
|
||||
vaultOcid: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Vault OCID required")
|
||||
.refine(
|
||||
(val) => new RE2("^ocid1\\.vault\\.oc1\\..+$").test(val),
|
||||
"Invalid Vault OCID format. Must start with ocid1.vault.oc1."
|
||||
)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.vaultOcid),
|
||||
keyOcid: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Key OCID required")
|
||||
.refine(
|
||||
(val) => new RE2("^ocid1\\.key\\.oc1\\..+$").test(val),
|
||||
"Invalid Key OCID format. Must start with ocid1.key.oc1."
|
||||
)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.OCI_VAULT.keyOcid)
|
||||
});
|
||||
|
||||
const OCIVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const OCIVaultSyncSchema = BaseSecretSyncSchema(SecretSync.OCIVault, OCIVaultSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.OCIVault),
|
||||
destinationConfig: OCIVaultSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateOCIVaultSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.OCIVault,
|
||||
OCIVaultSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: OCIVaultSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateOCIVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.OCIVault,
|
||||
OCIVaultSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: OCIVaultSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const OCIVaultSyncListItemSchema = z.object({
|
||||
name: z.literal("OCI Vault"),
|
||||
connection: z.literal(AppConnection.OCI),
|
||||
destination: z.literal(SecretSync.OCIVault),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
@@ -0,0 +1,48 @@
|
||||
import { SimpleAuthenticationDetailsProvider } from "oci-sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TOCIConnection } from "@app/services/app-connection/oci";
|
||||
|
||||
import { CreateOCIVaultSyncSchema, OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "./oci-vault-sync-schemas";
|
||||
|
||||
export type TOCIVaultSync = z.infer<typeof OCIVaultSyncSchema>;
|
||||
|
||||
export type TOCIVaultSyncInput = z.infer<typeof CreateOCIVaultSyncSchema>;
|
||||
|
||||
export type TOCIVaultSyncListItem = z.infer<typeof OCIVaultSyncListItemSchema>;
|
||||
|
||||
export type TOCIVaultSyncWithCredentials = TOCIVaultSync & {
|
||||
connection: TOCIConnection;
|
||||
};
|
||||
|
||||
export type TOCIVaultVariable = {
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TOCIVaultListVariables = {
|
||||
provider: SimpleAuthenticationDetailsProvider;
|
||||
compartmentId: string;
|
||||
vaultId: string;
|
||||
onlyActive?: boolean; // Whether to filter for only active secrets. Removes deleted / scheduled for deletion secrets
|
||||
};
|
||||
|
||||
export type TCreateOCIVaultVariable = TOCIVaultListVariables & {
|
||||
keyId: string;
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TUpdateOCIVaultVariable = TOCIVaultListVariables & {
|
||||
secretId: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type TDeleteOCIVaultVariable = TOCIVaultListVariables & {
|
||||
secretId: string;
|
||||
};
|
||||
|
||||
export type TUnmarkOCIVaultVariableFromDeletion = TOCIVaultListVariables & {
|
||||
secretId: string;
|
||||
};
|
@@ -12,7 +12,8 @@ export enum SecretSync {
|
||||
Vercel = "vercel",
|
||||
Windmill = "windmill",
|
||||
HCVault = "hashicorp-vault",
|
||||
TeamCity = "teamcity"
|
||||
TeamCity = "teamcity",
|
||||
OCIVault = "oci-vault"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
@@ -28,6 +28,7 @@ import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||
import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
|
||||
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||
import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "./oci-vault";
|
||||
import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
|
||||
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
|
||||
import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel";
|
||||
@@ -47,7 +48,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION,
|
||||
[SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION,
|
||||
[SecretSync.HCVault]: HC_VAULT_SYNC_LIST_OPTION,
|
||||
[SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION
|
||||
[SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION,
|
||||
[SecretSync.OCIVault]: OCI_VAULT_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretSyncOptions = () => {
|
||||
@@ -148,6 +150,8 @@ export const SecretSyncFns = {
|
||||
return HCVaultSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.TeamCity:
|
||||
return TeamCitySyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.OCIVault:
|
||||
return OCIVaultSyncFns.syncSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -213,6 +217,9 @@ export const SecretSyncFns = {
|
||||
case SecretSync.TeamCity:
|
||||
secretMap = await TeamCitySyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.OCIVault:
|
||||
secretMap = await OCIVaultSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -270,6 +277,8 @@ export const SecretSyncFns = {
|
||||
return HCVaultSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.TeamCity:
|
||||
return TeamCitySyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.OCIVault:
|
||||
return OCIVaultSyncFns.removeSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
|
@@ -15,7 +15,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.Vercel]: "Vercel",
|
||||
[SecretSync.Windmill]: "Windmill",
|
||||
[SecretSync.HCVault]: "Hashicorp Vault",
|
||||
[SecretSync.TeamCity]: "TeamCity"
|
||||
[SecretSync.TeamCity]: "TeamCity",
|
||||
[SecretSync.OCIVault]: "OCI Vault"
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
@@ -32,5 +33,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Vercel]: AppConnection.Vercel,
|
||||
[SecretSync.Windmill]: AppConnection.Windmill,
|
||||
[SecretSync.HCVault]: AppConnection.HCVault,
|
||||
[SecretSync.TeamCity]: AppConnection.TeamCity
|
||||
[SecretSync.TeamCity]: AppConnection.TeamCity,
|
||||
[SecretSync.OCIVault]: AppConnection.OCI
|
||||
};
|
||||
|
@@ -67,6 +67,7 @@ import {
|
||||
THumanitecSyncListItem,
|
||||
THumanitecSyncWithCredentials
|
||||
} from "./humanitec";
|
||||
import { TOCIVaultSync, TOCIVaultSyncInput, TOCIVaultSyncListItem, TOCIVaultSyncWithCredentials } from "./oci-vault";
|
||||
import {
|
||||
TTeamCitySync,
|
||||
TTeamCitySyncInput,
|
||||
@@ -95,7 +96,8 @@ export type TSecretSync =
|
||||
| TVercelSync
|
||||
| TWindmillSync
|
||||
| THCVaultSync
|
||||
| TTeamCitySync;
|
||||
| TTeamCitySync
|
||||
| TOCIVaultSync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
| TAwsParameterStoreSyncWithCredentials
|
||||
@@ -111,7 +113,8 @@ export type TSecretSyncWithCredentials =
|
||||
| TVercelSyncWithCredentials
|
||||
| TWindmillSyncWithCredentials
|
||||
| THCVaultSyncWithCredentials
|
||||
| TTeamCitySyncWithCredentials;
|
||||
| TTeamCitySyncWithCredentials
|
||||
| TOCIVaultSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput =
|
||||
| TAwsParameterStoreSyncInput
|
||||
@@ -127,7 +130,8 @@ export type TSecretSyncInput =
|
||||
| TVercelSyncInput
|
||||
| TWindmillSyncInput
|
||||
| THCVaultSyncInput
|
||||
| TTeamCitySyncInput;
|
||||
| TTeamCitySyncInput
|
||||
| TOCIVaultSyncInput;
|
||||
|
||||
export type TSecretSyncListItem =
|
||||
| TAwsParameterStoreSyncListItem
|
||||
@@ -143,7 +147,8 @@ export type TSecretSyncListItem =
|
||||
| TVercelSyncListItem
|
||||
| TWindmillSyncListItem
|
||||
| THCVaultSyncListItem
|
||||
| TTeamCitySyncListItem;
|
||||
| TTeamCitySyncListItem
|
||||
| TOCIVaultSyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
canImportSecrets: boolean;
|
||||
|
@@ -246,7 +246,8 @@ export const superAdminServiceFactory = ({
|
||||
|
||||
await microsoftTeamsService.initializeTeamsBot({
|
||||
botAppId: decryptedAppId.toString(),
|
||||
botAppPassword: decryptedAppPassword.toString()
|
||||
botAppPassword: decryptedAppPassword.toString(),
|
||||
lastUpdatedAt: updatedServerCfg.updatedAt
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -189,6 +189,7 @@ export type TSignCertificateEvent = {
|
||||
properties: {
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
subscriberId?: string;
|
||||
commonName: string;
|
||||
userAgent?: string;
|
||||
};
|
||||
@@ -199,6 +200,7 @@ export type TIssueCertificateEvent = {
|
||||
properties: {
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
subscriberId?: string;
|
||||
commonName: string;
|
||||
userAgent?: string;
|
||||
};
|
||||
|
@@ -1,115 +0,0 @@
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2019 Zachary Rice
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCommitAllowed(t *testing.T) {
|
||||
tests := []struct {
|
||||
allowlist Allowlist
|
||||
commit string
|
||||
commitAllowed bool
|
||||
}{
|
||||
{
|
||||
allowlist: Allowlist{
|
||||
Commits: []string{"commitA"},
|
||||
},
|
||||
commit: "commitA",
|
||||
commitAllowed: true,
|
||||
},
|
||||
{
|
||||
allowlist: Allowlist{
|
||||
Commits: []string{"commitB"},
|
||||
},
|
||||
commit: "commitA",
|
||||
commitAllowed: false,
|
||||
},
|
||||
{
|
||||
allowlist: Allowlist{
|
||||
Commits: []string{"commitB"},
|
||||
},
|
||||
commit: "",
|
||||
commitAllowed: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
assert.Equal(t, tt.commitAllowed, tt.allowlist.CommitAllowed(tt.commit))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexAllowed(t *testing.T) {
|
||||
tests := []struct {
|
||||
allowlist Allowlist
|
||||
secret string
|
||||
regexAllowed bool
|
||||
}{
|
||||
{
|
||||
allowlist: Allowlist{
|
||||
Regexes: []*regexp.Regexp{regexp.MustCompile("matchthis")},
|
||||
},
|
||||
secret: "a secret: matchthis, done",
|
||||
regexAllowed: true,
|
||||
},
|
||||
{
|
||||
allowlist: Allowlist{
|
||||
Regexes: []*regexp.Regexp{regexp.MustCompile("matchthis")},
|
||||
},
|
||||
secret: "a secret",
|
||||
regexAllowed: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
assert.Equal(t, tt.regexAllowed, tt.allowlist.RegexAllowed(tt.secret))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathAllowed(t *testing.T) {
|
||||
tests := []struct {
|
||||
allowlist Allowlist
|
||||
path string
|
||||
pathAllowed bool
|
||||
}{
|
||||
{
|
||||
allowlist: Allowlist{
|
||||
Paths: []*regexp.Regexp{regexp.MustCompile("path")},
|
||||
},
|
||||
path: "a path",
|
||||
pathAllowed: true,
|
||||
},
|
||||
{
|
||||
allowlist: Allowlist{
|
||||
Paths: []*regexp.Regexp{regexp.MustCompile("path")},
|
||||
},
|
||||
path: "a ???",
|
||||
pathAllowed: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
assert.Equal(t, tt.pathAllowed, tt.allowlist.PathAllowed(tt.path))
|
||||
}
|
||||
}
|
@@ -1,279 +0,0 @@
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2019 Zachary Rice
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
//go:embed infisical-scan.toml
|
||||
var DefaultConfig string
|
||||
|
||||
// use to keep track of how many configs we can extend
|
||||
// yea I know, globals bad
|
||||
var extendDepth int
|
||||
|
||||
const maxExtendDepth = 2
|
||||
|
||||
const DefaultScanConfigFileName = ".infisical-scan.toml"
|
||||
const DefaultScanConfigEnvName = "INFISICAL_SCAN_CONFIG"
|
||||
const DefaultInfisicalIgnoreFineName = ".infisicalignore"
|
||||
|
||||
// ViperConfig is the config struct used by the Viper config package
|
||||
// to parse the config file. This struct does not include regular expressions.
|
||||
// It is used as an intermediary to convert the Viper config to the Config struct.
|
||||
type ViperConfig struct {
|
||||
Description string
|
||||
Extend Extend
|
||||
Rules []struct {
|
||||
ID string
|
||||
Description string
|
||||
Entropy float64
|
||||
SecretGroup int
|
||||
Regex string
|
||||
Keywords []string
|
||||
Path string
|
||||
Tags []string
|
||||
|
||||
Allowlist struct {
|
||||
RegexTarget string
|
||||
Regexes []string
|
||||
Paths []string
|
||||
Commits []string
|
||||
StopWords []string
|
||||
}
|
||||
}
|
||||
Allowlist struct {
|
||||
RegexTarget string
|
||||
Regexes []string
|
||||
Paths []string
|
||||
Commits []string
|
||||
StopWords []string
|
||||
}
|
||||
}
|
||||
|
||||
// Config is a configuration struct that contains rules and an allowlist if present.
|
||||
type Config struct {
|
||||
Extend Extend
|
||||
Path string
|
||||
Description string
|
||||
Rules map[string]Rule
|
||||
Allowlist Allowlist
|
||||
Keywords []string
|
||||
|
||||
// used to keep sarif results consistent
|
||||
orderedRules []string
|
||||
}
|
||||
|
||||
// Extend is a struct that allows users to define how they want their
|
||||
// configuration extended by other configuration files.
|
||||
type Extend struct {
|
||||
Path string
|
||||
URL string
|
||||
UseDefault bool
|
||||
}
|
||||
|
||||
func (vc *ViperConfig) Translate() (Config, error) {
|
||||
var (
|
||||
keywords []string
|
||||
orderedRules []string
|
||||
)
|
||||
rulesMap := make(map[string]Rule)
|
||||
|
||||
for _, r := range vc.Rules {
|
||||
var allowlistRegexes []*regexp.Regexp
|
||||
for _, a := range r.Allowlist.Regexes {
|
||||
allowlistRegexes = append(allowlistRegexes, regexp.MustCompile(a))
|
||||
}
|
||||
var allowlistPaths []*regexp.Regexp
|
||||
for _, a := range r.Allowlist.Paths {
|
||||
allowlistPaths = append(allowlistPaths, regexp.MustCompile(a))
|
||||
}
|
||||
|
||||
if r.Keywords == nil {
|
||||
r.Keywords = []string{}
|
||||
} else {
|
||||
for _, k := range r.Keywords {
|
||||
keywords = append(keywords, strings.ToLower(k))
|
||||
}
|
||||
}
|
||||
|
||||
if r.Tags == nil {
|
||||
r.Tags = []string{}
|
||||
}
|
||||
|
||||
var configRegex *regexp.Regexp
|
||||
var configPathRegex *regexp.Regexp
|
||||
if r.Regex == "" {
|
||||
configRegex = nil
|
||||
} else {
|
||||
configRegex = regexp.MustCompile(r.Regex)
|
||||
}
|
||||
if r.Path == "" {
|
||||
configPathRegex = nil
|
||||
} else {
|
||||
configPathRegex = regexp.MustCompile(r.Path)
|
||||
}
|
||||
r := Rule{
|
||||
Description: r.Description,
|
||||
RuleID: r.ID,
|
||||
Regex: configRegex,
|
||||
Path: configPathRegex,
|
||||
SecretGroup: r.SecretGroup,
|
||||
Entropy: r.Entropy,
|
||||
Tags: r.Tags,
|
||||
Keywords: r.Keywords,
|
||||
Allowlist: Allowlist{
|
||||
RegexTarget: r.Allowlist.RegexTarget,
|
||||
Regexes: allowlistRegexes,
|
||||
Paths: allowlistPaths,
|
||||
Commits: r.Allowlist.Commits,
|
||||
StopWords: r.Allowlist.StopWords,
|
||||
},
|
||||
}
|
||||
orderedRules = append(orderedRules, r.RuleID)
|
||||
|
||||
if r.Regex != nil && r.SecretGroup > r.Regex.NumSubexp() {
|
||||
return Config{}, fmt.Errorf("%s invalid regex secret group %d, max regex secret group %d", r.Description, r.SecretGroup, r.Regex.NumSubexp())
|
||||
}
|
||||
rulesMap[r.RuleID] = r
|
||||
}
|
||||
var allowlistRegexes []*regexp.Regexp
|
||||
for _, a := range vc.Allowlist.Regexes {
|
||||
allowlistRegexes = append(allowlistRegexes, regexp.MustCompile(a))
|
||||
}
|
||||
var allowlistPaths []*regexp.Regexp
|
||||
for _, a := range vc.Allowlist.Paths {
|
||||
allowlistPaths = append(allowlistPaths, regexp.MustCompile(a))
|
||||
}
|
||||
c := Config{
|
||||
Description: vc.Description,
|
||||
Extend: vc.Extend,
|
||||
Rules: rulesMap,
|
||||
Allowlist: Allowlist{
|
||||
RegexTarget: vc.Allowlist.RegexTarget,
|
||||
Regexes: allowlistRegexes,
|
||||
Paths: allowlistPaths,
|
||||
Commits: vc.Allowlist.Commits,
|
||||
StopWords: vc.Allowlist.StopWords,
|
||||
},
|
||||
Keywords: keywords,
|
||||
orderedRules: orderedRules,
|
||||
}
|
||||
|
||||
if maxExtendDepth != extendDepth {
|
||||
// disallow both usedefault and path from being set
|
||||
if c.Extend.Path != "" && c.Extend.UseDefault {
|
||||
log.Fatal().Msg("unable to load config due to extend.path and extend.useDefault being set")
|
||||
}
|
||||
if c.Extend.UseDefault {
|
||||
c.extendDefault()
|
||||
} else if c.Extend.Path != "" {
|
||||
c.extendPath()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Config) OrderedRules() []Rule {
|
||||
var orderedRules []Rule
|
||||
for _, id := range c.orderedRules {
|
||||
if _, ok := c.Rules[id]; ok {
|
||||
orderedRules = append(orderedRules, c.Rules[id])
|
||||
}
|
||||
}
|
||||
return orderedRules
|
||||
}
|
||||
|
||||
func (c *Config) extendDefault() {
|
||||
extendDepth++
|
||||
viper.SetConfigType("toml")
|
||||
if err := viper.ReadConfig(strings.NewReader(DefaultConfig)); err != nil {
|
||||
log.Fatal().Msgf("failed to load extended config, err: %s", err)
|
||||
return
|
||||
}
|
||||
defaultViperConfig := ViperConfig{}
|
||||
if err := viper.Unmarshal(&defaultViperConfig); err != nil {
|
||||
log.Fatal().Msgf("failed to load extended config, err: %s", err)
|
||||
return
|
||||
}
|
||||
cfg, err := defaultViperConfig.Translate()
|
||||
if err != nil {
|
||||
log.Fatal().Msgf("failed to load extended config, err: %s", err)
|
||||
return
|
||||
}
|
||||
log.Debug().Msg("extending config with default config")
|
||||
c.extend(cfg)
|
||||
|
||||
}
|
||||
|
||||
func (c *Config) extendPath() {
|
||||
extendDepth++
|
||||
viper.SetConfigFile(c.Extend.Path)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Fatal().Msgf("failed to load extended config, err: %s", err)
|
||||
return
|
||||
}
|
||||
extensionViperConfig := ViperConfig{}
|
||||
if err := viper.Unmarshal(&extensionViperConfig); err != nil {
|
||||
log.Fatal().Msgf("failed to load extended config, err: %s", err)
|
||||
return
|
||||
}
|
||||
cfg, err := extensionViperConfig.Translate()
|
||||
if err != nil {
|
||||
log.Fatal().Msgf("failed to load extended config, err: %s", err)
|
||||
return
|
||||
}
|
||||
log.Debug().Msgf("extending config with %s", c.Extend.Path)
|
||||
c.extend(cfg)
|
||||
}
|
||||
|
||||
func (c *Config) extendURL() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func (c *Config) extend(extensionConfig Config) {
|
||||
for ruleID, rule := range extensionConfig.Rules {
|
||||
if _, ok := c.Rules[ruleID]; !ok {
|
||||
log.Trace().Msgf("adding %s to base config", ruleID)
|
||||
c.Rules[ruleID] = rule
|
||||
c.Keywords = append(c.Keywords, rule.Keywords...)
|
||||
}
|
||||
}
|
||||
|
||||
// append allowlists, not attempting to merge
|
||||
c.Allowlist.Commits = append(c.Allowlist.Commits,
|
||||
extensionConfig.Allowlist.Commits...)
|
||||
c.Allowlist.Paths = append(c.Allowlist.Paths,
|
||||
extensionConfig.Allowlist.Paths...)
|
||||
c.Allowlist.Regexes = append(c.Allowlist.Regexes,
|
||||
extensionConfig.Allowlist.Regexes...)
|
||||
}
|
@@ -1,170 +0,0 @@
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2019 Zachary Rice
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const configPath = "../testdata/config/"
|
||||
|
||||
func TestTranslate(t *testing.T) {
|
||||
tests := []struct {
|
||||
cfgName string
|
||||
cfg Config
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
cfgName: "allow_aws_re",
|
||||
cfg: Config{
|
||||
Rules: map[string]Rule{"aws-access-key": {
|
||||
Description: "AWS Access Key",
|
||||
Regex: regexp.MustCompile("(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}"),
|
||||
Tags: []string{"key", "AWS"},
|
||||
Keywords: []string{},
|
||||
RuleID: "aws-access-key",
|
||||
Allowlist: Allowlist{
|
||||
Regexes: []*regexp.Regexp{
|
||||
regexp.MustCompile("AKIALALEMEL33243OLIA"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cfgName: "allow_commit",
|
||||
cfg: Config{
|
||||
Rules: map[string]Rule{"aws-access-key": {
|
||||
Description: "AWS Access Key",
|
||||
Regex: regexp.MustCompile("(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}"),
|
||||
Tags: []string{"key", "AWS"},
|
||||
Keywords: []string{},
|
||||
RuleID: "aws-access-key",
|
||||
Allowlist: Allowlist{
|
||||
Commits: []string{"allowthiscommit"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cfgName: "allow_path",
|
||||
cfg: Config{
|
||||
Rules: map[string]Rule{"aws-access-key": {
|
||||
Description: "AWS Access Key",
|
||||
Regex: regexp.MustCompile("(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}"),
|
||||
Tags: []string{"key", "AWS"},
|
||||
Keywords: []string{},
|
||||
RuleID: "aws-access-key",
|
||||
Allowlist: Allowlist{
|
||||
Paths: []*regexp.Regexp{
|
||||
regexp.MustCompile(".go"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cfgName: "entropy_group",
|
||||
cfg: Config{
|
||||
Rules: map[string]Rule{"discord-api-key": {
|
||||
Description: "Discord API key",
|
||||
Regex: regexp.MustCompile(`(?i)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]`),
|
||||
RuleID: "discord-api-key",
|
||||
Allowlist: Allowlist{},
|
||||
Entropy: 3.5,
|
||||
SecretGroup: 3,
|
||||
Tags: []string{},
|
||||
Keywords: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cfgName: "bad_entropy_group",
|
||||
cfg: Config{},
|
||||
wantError: fmt.Errorf("Discord API key invalid regex secret group 5, max regex secret group 3"),
|
||||
},
|
||||
{
|
||||
cfgName: "base",
|
||||
cfg: Config{
|
||||
Rules: map[string]Rule{
|
||||
"aws-access-key": {
|
||||
Description: "AWS Access Key",
|
||||
Regex: regexp.MustCompile("(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}"),
|
||||
Tags: []string{"key", "AWS"},
|
||||
Keywords: []string{},
|
||||
RuleID: "aws-access-key",
|
||||
},
|
||||
"aws-secret-key": {
|
||||
Description: "AWS Secret Key",
|
||||
Regex: regexp.MustCompile(`(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}`),
|
||||
Tags: []string{"key", "AWS"},
|
||||
Keywords: []string{},
|
||||
RuleID: "aws-secret-key",
|
||||
},
|
||||
"aws-secret-key-again": {
|
||||
Description: "AWS Secret Key",
|
||||
Regex: regexp.MustCompile(`(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}`),
|
||||
Tags: []string{"key", "AWS"},
|
||||
Keywords: []string{},
|
||||
RuleID: "aws-secret-key-again",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
viper.Reset()
|
||||
viper.AddConfigPath(configPath)
|
||||
viper.SetConfigName(tt.cfgName)
|
||||
viper.SetConfigType("toml")
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var vc ViperConfig
|
||||
err = viper.Unmarshal(&vc)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
cfg, err := vc.Translate()
|
||||
if tt.wantError != nil {
|
||||
if err == nil {
|
||||
t.Errorf("expected error")
|
||||
}
|
||||
assert.Equal(t, tt.wantError, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, cfg.Rules, tt.cfg.Rules)
|
||||
}
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
public_ip: 127.0.0.1
|
||||
auth_secret: example-auth-secret
|
||||
realm: infisical.org
|
||||
# set port 5349 for tls
|
||||
# port: 5349
|
||||
# tls_private_key_path: /full-path
|
||||
# tls_ca_path: /full-path
|
||||
# tls_cert_path: /full-path
|
@@ -1,8 +0,0 @@
|
||||
public_ip: 127.0.0.1
|
||||
auth_secret: changeThisOnProduction
|
||||
realm: infisical.org
|
||||
# set port 5349 for tls
|
||||
# port: 5349
|
||||
# tls_private_key_path: /full-path
|
||||
# tls_ca_path: /full-path
|
||||
# tls_cert_path: /full-path
|
File diff suppressed because it is too large
Load Diff
@@ -1,43 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Rules contain information that define details on how to detect secrets
|
||||
type Rule struct {
|
||||
// Description is the description of the rule.
|
||||
Description string
|
||||
|
||||
// RuleID is a unique identifier for this rule
|
||||
RuleID string
|
||||
|
||||
// Entropy is a float representing the minimum shannon
|
||||
// entropy a regex group must have to be considered a secret.
|
||||
Entropy float64
|
||||
|
||||
// SecretGroup is an int used to extract secret from regex
|
||||
// match and used as the group that will have its entropy
|
||||
// checked if `entropy` is set.
|
||||
SecretGroup int
|
||||
|
||||
// Regex is a golang regular expression used to detect secrets.
|
||||
Regex *regexp.Regexp
|
||||
|
||||
// Path is a golang regular expression used to
|
||||
// filter secrets by path
|
||||
Path *regexp.Regexp
|
||||
|
||||
// Tags is an array of strings used for metadata
|
||||
// and reporting purposes.
|
||||
Tags []string
|
||||
|
||||
// Keywords are used for pre-regex check filtering. Rules that contain
|
||||
// keywords will perform a quick string compare check to make sure the
|
||||
// keyword(s) are in the content being scanned.
|
||||
Keywords []string
|
||||
|
||||
// Allowlist allows a rule to be ignored for specific
|
||||
// regexes, paths, and/or commits
|
||||
Allowlist Allowlist
|
||||
}
|
@@ -1,24 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func anyRegexMatch(f string, res []*regexp.Regexp) bool {
|
||||
for _, re := range res {
|
||||
if regexMatched(f, re) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func regexMatched(f string, re *regexp.Regexp) bool {
|
||||
if re == nil {
|
||||
return false
|
||||
}
|
||||
if re.FindString(f) != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
@@ -25,35 +25,31 @@ package detect
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/Infisical/infisical-merge/report"
|
||||
"github.com/Infisical/infisical-merge/detect/report"
|
||||
)
|
||||
|
||||
func IsNew(finding report.Finding, baseline []report.Finding) bool {
|
||||
func IsNew(finding report.Finding, redact uint, baseline []report.Finding) bool {
|
||||
// Explicitly testing each property as it gives significantly better performance in comparison to cmp.Equal(). Drawback is that
|
||||
// the code requires maintanance if/when the Finding struct changes
|
||||
// the code requires maintenance if/when the Finding struct changes
|
||||
for _, b := range baseline {
|
||||
|
||||
if finding.Author == b.Author &&
|
||||
finding.Commit == b.Commit &&
|
||||
finding.Date == b.Date &&
|
||||
if finding.RuleID == b.RuleID &&
|
||||
finding.Description == b.Description &&
|
||||
finding.Email == b.Email &&
|
||||
finding.EndColumn == b.EndColumn &&
|
||||
finding.StartLine == b.StartLine &&
|
||||
finding.EndLine == b.EndLine &&
|
||||
finding.Entropy == b.Entropy &&
|
||||
finding.File == b.File &&
|
||||
// Omit checking finding.Fingerprint - if the format of the fingerprint changes, the users will see unexpected behaviour
|
||||
finding.Match == b.Match &&
|
||||
finding.Message == b.Message &&
|
||||
finding.RuleID == b.RuleID &&
|
||||
finding.Secret == b.Secret &&
|
||||
finding.StartColumn == b.StartColumn &&
|
||||
finding.StartLine == b.StartLine {
|
||||
finding.EndColumn == b.EndColumn &&
|
||||
(redact > 0 || (finding.Match == b.Match && finding.Secret == b.Secret)) &&
|
||||
finding.File == b.File &&
|
||||
finding.Commit == b.Commit &&
|
||||
finding.Author == b.Author &&
|
||||
finding.Email == b.Email &&
|
||||
finding.Date == b.Date &&
|
||||
finding.Message == b.Message &&
|
||||
// Omit checking finding.Fingerprint - if the format of the fingerprint changes, the users will see unexpected behaviour
|
||||
finding.Entropy == b.Entropy {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -61,23 +57,12 @@ func IsNew(finding report.Finding, baseline []report.Finding) bool {
|
||||
}
|
||||
|
||||
func LoadBaseline(baselinePath string) ([]report.Finding, error) {
|
||||
var previousFindings []report.Finding
|
||||
jsonFile, err := os.Open(baselinePath)
|
||||
bytes, err := os.ReadFile(baselinePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open %s", baselinePath)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if cerr := jsonFile.Close(); cerr != nil {
|
||||
log.Warn().Err(cerr).Msg("problem closing jsonFile handle")
|
||||
}
|
||||
}()
|
||||
|
||||
bytes, err := io.ReadAll(jsonFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read data from the file %s", baselinePath)
|
||||
}
|
||||
|
||||
var previousFindings []report.Finding
|
||||
err = json.Unmarshal(bytes, &previousFindings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the format of the file %s is not supported", baselinePath)
|
||||
@@ -85,3 +70,34 @@ func LoadBaseline(baselinePath string) ([]report.Finding, error) {
|
||||
|
||||
return previousFindings, nil
|
||||
}
|
||||
|
||||
func (d *Detector) AddBaseline(baselinePath string, source string) error {
|
||||
if baselinePath != "" {
|
||||
absoluteSource, err := filepath.Abs(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
absoluteBaseline, err := filepath.Abs(baselinePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relativeBaseline, err := filepath.Rel(absoluteSource, absoluteBaseline)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseline, err := LoadBaseline(baselinePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.baseline = baseline
|
||||
baselinePath = relativeBaseline
|
||||
|
||||
}
|
||||
|
||||
d.baselinePath = baselinePath
|
||||
return nil
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user