mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-24 20:43:19 +00:00
Compare commits
131 Commits
misc/add-p
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
eba12912f8 | ||
|
80edccc953 | ||
|
f1b1d6f480 | ||
|
07d6616f3c | ||
|
28d056cf7a | ||
|
f5d7809515 | ||
|
233740e029 | ||
|
767fdc645f | ||
|
c477703dda | ||
|
923d639c40 | ||
|
7655dc7f3c | ||
|
6c6c4db92c | ||
|
8cf125ed32 | ||
|
886cc9a113 | ||
|
e1016f0a8b | ||
|
9c0a5f0bd4 | ||
|
7facd0e89e | ||
|
3afe2552d5 | ||
|
1fdb695240 | ||
|
d9bd1ac878 | ||
|
ee185cbe47 | ||
|
abc2f3808e | ||
|
733440a7b5 | ||
|
1ef3525917 | ||
|
6664add428 | ||
|
242e8fd2c6 | ||
|
1137247e69 | ||
|
32b951f6e9 | ||
|
6f5fe053cd | ||
|
875ec6a24e | ||
|
17233e6a6f | ||
|
0dd06c1d66 | ||
|
fc2e5d18b7 | ||
|
ae1ee25687 | ||
|
5d0bbce12d | ||
|
8c87c40467 | ||
|
a9dab557d9 | ||
|
76c3f1c152 | ||
|
965084cc0c | ||
|
4650ba9fdd | ||
|
73dea6a0be | ||
|
e7742afcd3 | ||
|
7d3dd765ad | ||
|
927eb0407d | ||
|
17ddb79def | ||
|
5ef5a5a107 | ||
|
9ae0880f50 | ||
|
3814c65f38 | ||
|
3fa98e2a8d | ||
|
c6b21491db | ||
|
357381b0d6 | ||
|
82af77c480 | ||
|
b2fae5c439 | ||
|
f16e96759f | ||
|
5eb9a1a667 | ||
|
03ad6f822a | ||
|
23a5a7a624 | ||
|
98447e9402 | ||
|
0f7e8585dc | ||
|
8568d1f6fe | ||
|
27198869d8 | ||
|
dd0880825b | ||
|
f27050a1c3 | ||
|
785173747f | ||
|
d33b06dd8a | ||
|
9a6e27d4be | ||
|
d0db5c00e8 | ||
|
9475c1671e | ||
|
0f710b1ccc | ||
|
71c55d5a53 | ||
|
32bca651df | ||
|
82533f49ca | ||
|
1d8c513da1 | ||
|
ae8a78b883 | ||
|
b08b53b77d | ||
|
862ed4f4e7 | ||
|
7b9254d09a | ||
|
c6305045e3 | ||
|
24bf9f7a2a | ||
|
86d7fca8fb | ||
|
cac4f30ca8 | ||
|
101c056f43 | ||
|
8d4fa0bdb9 | ||
|
2642f7501d | ||
|
68ba807b43 | ||
|
80352acc8a | ||
|
499ff3635b | ||
|
78fc8a693d | ||
|
78687984b7 | ||
|
25d3fb6a8c | ||
|
31a4bcafbe | ||
|
ac8b3aca60 | ||
|
4ea0cc62e3 | ||
|
bdab16f64b | ||
|
9d0020fa4e | ||
|
3c07204532 | ||
|
c0926bec69 | ||
|
b9d74e0aed | ||
|
f3078040fc | ||
|
f2fead7a51 | ||
|
3c58bf890d | ||
|
dc219b8e9f | ||
|
f1e30fd06b | ||
|
e339b81bf1 | ||
|
b9bfe19b64 | ||
|
fa030417ef | ||
|
8bfbae1037 | ||
|
d00b34663e | ||
|
581e4b35f9 | ||
|
f33a777fae | ||
|
8a870131e9 | ||
|
d97057b43b | ||
|
19b0cd9735 | ||
|
7dcd3d24aa | ||
|
3c5c6aeca8 | ||
|
1ec87fae75 | ||
|
aec131543f | ||
|
aeaa5babab | ||
|
07898414a3 | ||
|
f15b30ff85 | ||
|
8ee2b54182 | ||
|
b121ec891f | ||
|
ab566bcbe4 | ||
|
041d585f19 | ||
|
224b167000 | ||
|
e1a11c37e3 | ||
|
15130a433c | ||
|
a0bf03b2ae | ||
|
4d8598a019 | ||
|
a9da2d6241 | ||
|
4420985669 |
@@ -145,3 +145,9 @@ jobs:
|
|||||||
INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }}
|
INFISICAL_CLI_REPO_SIGNING_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_SIGNING_KEY_ID }}
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
- name: Invalidate Cloudfront cache
|
||||||
|
run: aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths '/deb/dists/stable/*'
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.INFISICAL_CLI_REPO_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.INFISICAL_CLI_REPO_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
CLOUDFRONT_DISTRIBUTION_ID: ${{ secrets.INFISICAL_CLI_REPO_CLOUDFRONT_DISTRIBUTION_ID }}
|
||||||
|
@@ -22,3 +22,5 @@ frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredent
|
|||||||
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:28
|
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:28
|
||||||
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
|
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
|
||||||
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
||||||
|
docs/documentation/platform/kms/overview.mdx:generic-api-key:281
|
||||||
|
docs/documentation/platform/kms/overview.mdx:generic-api-key:344
|
||||||
|
@@ -50,7 +50,7 @@ We're on a mission to make security tooling more accessible to everyone, not jus
|
|||||||
- **[Dashboard](https://infisical.com/docs/documentation/platform/project)**: Manage secrets across projects and environments (e.g. development, production, etc.) through a user-friendly interface.
|
- **[Dashboard](https://infisical.com/docs/documentation/platform/project)**: Manage secrets across projects and environments (e.g. development, production, etc.) through a user-friendly interface.
|
||||||
- **[Native Integrations](https://infisical.com/docs/integrations/overview)**: Sync secrets to platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and use tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
|
- **[Native Integrations](https://infisical.com/docs/integrations/overview)**: Sync secrets to platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and use tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
|
||||||
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)**: Keep track of every secret and project state; roll back when needed.
|
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)**: Keep track of every secret and project state; roll back when needed.
|
||||||
- **[Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview)**: Rotate secrets at regular intervals for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/secret-rotation/postgres), [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql), [AWS IAM](https://infisical.com/docs/documentation/platform/secret-rotation/aws-iam), and more.
|
- **[Secret Rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview)**: Rotate secrets at regular intervals for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/secret-rotation/postgres-credentials), [MySQL](https://infisical.com/docs/documentation/platform/secret-rotation/mysql), [AWS IAM](https://infisical.com/docs/documentation/platform/secret-rotation/aws-iam), and more.
|
||||||
- **[Dynamic Secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview)**: Generate ephemeral secrets on-demand for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/postgresql), [MySQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/mysql), [RabbitMQ](https://infisical.com/docs/documentation/platform/dynamic-secrets/rabbit-mq), and more.
|
- **[Dynamic Secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview)**: Generate ephemeral secrets on-demand for services like [PostgreSQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/postgresql), [MySQL](https://infisical.com/docs/documentation/platform/dynamic-secrets/mysql), [RabbitMQ](https://infisical.com/docs/documentation/platform/dynamic-secrets/rabbit-mq), and more.
|
||||||
- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)**: Prevent secrets from leaking to git.
|
- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)**: Prevent secrets from leaking to git.
|
||||||
- **[Infisical Kubernetes Operator](https://infisical.com/docs/documentation/getting-started/kubernetes)**: Deliver secrets to your Kubernetes workloads and automatically reload deployments.
|
- **[Infisical Kubernetes Operator](https://infisical.com/docs/documentation/getting-started/kubernetes)**: Deliver secrets to your Kubernetes workloads and automatically reload deployments.
|
||||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -136,7 +136,7 @@ declare module "fastify" {
|
|||||||
rateLimits: RateLimitConfiguration;
|
rateLimits: RateLimitConfiguration;
|
||||||
// passport data
|
// passport data
|
||||||
passportUser: {
|
passportUser: {
|
||||||
isUserCompleted: string;
|
isUserCompleted: boolean;
|
||||||
providerAuthToken: string;
|
providerAuthToken: string;
|
||||||
};
|
};
|
||||||
kmipUser: {
|
kmipUser: {
|
||||||
|
@@ -5,15 +5,21 @@ import { KmsKeyUsage } from "@app/services/kms/kms-types";
|
|||||||
import { TableName } from "../schemas";
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
const hasTypeColumn = await knex.schema.hasColumn(TableName.KmsKey, "type");
|
const hasKeyUsageColumn = await knex.schema.hasColumn(TableName.KmsKey, "keyUsage");
|
||||||
|
|
||||||
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
if (!hasKeyUsageColumn) {
|
||||||
if (!hasTypeColumn) t.string("keyUsage").notNullable().defaultTo(KmsKeyUsage.ENCRYPT_DECRYPT);
|
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||||
});
|
t.string("keyUsage").notNullable().defaultTo(KmsKeyUsage.ENCRYPT_DECRYPT);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
export async function down(knex: Knex): Promise<void> {
|
||||||
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
const hasKeyUsageColumn = await knex.schema.hasColumn(TableName.KmsKey, "keyUsage");
|
||||||
t.dropColumn("keyUsage");
|
|
||||||
});
|
if (hasKeyUsageColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.KmsKey, (t) => {
|
||||||
|
t.dropColumn("keyUsage");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,20 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.ResourceMetadata, "dynamicSecretId"))) {
|
||||||
|
await knex.schema.alterTable(TableName.ResourceMetadata, (tb) => {
|
||||||
|
tb.uuid("dynamicSecretId");
|
||||||
|
tb.foreign("dynamicSecretId").references("id").inTable(TableName.DynamicSecret).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.ResourceMetadata, "dynamicSecretId")) {
|
||||||
|
await knex.schema.alterTable(TableName.ResourceMetadata, (tb) => {
|
||||||
|
tb.dropColumn("dynamicSecretId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent");
|
||||||
|
if (!hasCol) {
|
||||||
|
await knex.schema.alterTable(TableName.ServiceToken, (t) => {
|
||||||
|
t.boolean("expiryNotificationSent").defaultTo(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update only tokens where expiresAt is before current time
|
||||||
|
await knex(TableName.ServiceToken)
|
||||||
|
.whereRaw(`${TableName.ServiceToken}."expiresAt" < NOW()`)
|
||||||
|
.whereNotNull("expiresAt")
|
||||||
|
.update({ expiryNotificationSent: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasCol = await knex.schema.hasColumn(TableName.ServiceToken, "expiryNotificationSent");
|
||||||
|
if (hasCol) {
|
||||||
|
await knex.schema.alterTable(TableName.ServiceToken, (t) => {
|
||||||
|
t.dropColumn("expiryNotificationSent");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasCol = await knex.schema.hasColumn(TableName.Project, "hasDeleteProtection");
|
||||||
|
if (!hasCol) {
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
t.boolean("hasDeleteProtection").defaultTo(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasCol = await knex.schema.hasColumn(TableName.Project, "hasDeleteProtection");
|
||||||
|
if (hasCol) {
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
t.dropColumn("hasDeleteProtection");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
|
t.string("altNames", 4096).alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
|
t.string("altNames").alter(); // Defaults to varchar(255)
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.KmipOrgServerCertificates, (t) => {
|
||||||
|
t.string("altNames", 4096).alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.KmipOrgServerCertificates, (t) => {
|
||||||
|
t.string("altNames").alter(); // Defaults to varchar(255)
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm"))) {
|
||||||
|
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
|
||||||
|
t.string("jwtSignatureAlgorithm").defaultTo(OIDCJWTSignatureAlgorithm.RS256).notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.OidcConfig, "jwtSignatureAlgorithm")) {
|
||||||
|
await knex.schema.alterTable(TableName.OidcConfig, (t) => {
|
||||||
|
t.dropColumn("jwtSignatureAlgorithm");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.Organization, "bypassOrgAuthEnabled"))) {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.boolean("bypassOrgAuthEnabled").defaultTo(false).notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.Organization, "bypassOrgAuthEnabled")) {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.dropColumn("bypassOrgAuthEnabled");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -30,9 +30,10 @@ export const OidcConfigsSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
lastUsed: z.date().nullable().optional(),
|
lastUsed: z.date().nullable().optional(),
|
||||||
manageGroupMemberships: z.boolean().default(false),
|
|
||||||
encryptedOidcClientId: zodBuffer,
|
encryptedOidcClientId: zodBuffer,
|
||||||
encryptedOidcClientSecret: zodBuffer
|
encryptedOidcClientSecret: zodBuffer,
|
||||||
|
manageGroupMemberships: z.boolean().default(false),
|
||||||
|
jwtSignatureAlgorithm: z.string().default("RS256")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
|
||||||
|
@@ -26,7 +26,8 @@ export const OrganizationsSchema = z.object({
|
|||||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional(),
|
||||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional()
|
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
||||||
|
bypassOrgAuthEnabled: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
@@ -26,7 +26,8 @@ export const ProjectsSchema = z.object({
|
|||||||
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
|
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
|
||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
enforceCapitalization: z.boolean().default(false)
|
enforceCapitalization: z.boolean().default(false),
|
||||||
|
hasDeleteProtection: z.boolean().default(true).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
@@ -16,7 +16,8 @@ export const ResourceMetadataSchema = z.object({
|
|||||||
identityId: z.string().uuid().nullable().optional(),
|
identityId: z.string().uuid().nullable().optional(),
|
||||||
secretId: z.string().uuid().nullable().optional(),
|
secretId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
dynamicSecretId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TResourceMetadata = z.infer<typeof ResourceMetadataSchema>;
|
export type TResourceMetadata = z.infer<typeof ResourceMetadataSchema>;
|
||||||
|
@@ -21,7 +21,8 @@ export const ServiceTokensSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
createdBy: z.string(),
|
createdBy: z.string(),
|
||||||
projectId: z.string()
|
projectId: z.string(),
|
||||||
|
expiryNotificationSent: z.boolean().default(false).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TServiceTokens = z.infer<typeof ServiceTokensSchema>;
|
export type TServiceTokens = z.infer<typeof ServiceTokensSchema>;
|
||||||
|
@@ -11,6 +11,7 @@ import { slugSchema } from "@app/server/lib/schemas";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
|
|
||||||
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -48,7 +49,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
.nullable(),
|
.nullable(),
|
||||||
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
||||||
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
|
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
|
||||||
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name)
|
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name),
|
||||||
|
metadata: ResourceMetadataSchema.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -143,7 +145,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
})
|
})
|
||||||
.nullable(),
|
.nullable(),
|
||||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional()
|
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
|
||||||
|
metadata: ResourceMetadataSchema.optional()
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -238,6 +241,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
name: req.params.name,
|
name: req.params.name,
|
||||||
...req.query
|
...req.query
|
||||||
});
|
});
|
||||||
|
|
||||||
return { dynamicSecret: dynamicSecretCfg };
|
return { dynamicSecret: dynamicSecretCfg };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -12,7 +12,7 @@ import RedisStore from "connect-redis";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { OidcConfigsSchema } from "@app/db/schemas";
|
import { OidcConfigsSchema } from "@app/db/schemas";
|
||||||
import { OIDCConfigurationType } from "@app/ee/services/oidc/oidc-config-types";
|
import { OIDCConfigurationType, OIDCJWTSignatureAlgorithm } from "@app/ee/services/oidc/oidc-config-types";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -30,7 +30,8 @@ const SanitizedOidcConfigSchema = OidcConfigsSchema.pick({
|
|||||||
orgId: true,
|
orgId: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
allowedEmailDomains: true,
|
allowedEmailDomains: true,
|
||||||
manageGroupMemberships: true
|
manageGroupMemberships: true,
|
||||||
|
jwtSignatureAlgorithm: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
||||||
@@ -170,7 +171,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
orgId: true,
|
orgId: true,
|
||||||
allowedEmailDomains: true,
|
allowedEmailDomains: true,
|
||||||
manageGroupMemberships: true
|
manageGroupMemberships: true,
|
||||||
|
jwtSignatureAlgorithm: true
|
||||||
}).extend({
|
}).extend({
|
||||||
clientId: z.string(),
|
clientId: z.string(),
|
||||||
clientSecret: z.string()
|
clientSecret: z.string()
|
||||||
@@ -225,7 +227,8 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
clientId: z.string().trim(),
|
clientId: z.string().trim(),
|
||||||
clientSecret: z.string().trim(),
|
clientSecret: z.string().trim(),
|
||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
manageGroupMemberships: z.boolean().optional()
|
manageGroupMemberships: z.boolean().optional(),
|
||||||
|
jwtSignatureAlgorithm: z.nativeEnum(OIDCJWTSignatureAlgorithm).optional()
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.merge(z.object({ orgSlug: z.string() })),
|
.merge(z.object({ orgSlug: z.string() })),
|
||||||
@@ -292,7 +295,11 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
|
|||||||
clientSecret: z.string().trim(),
|
clientSecret: z.string().trim(),
|
||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
orgSlug: z.string().trim(),
|
orgSlug: z.string().trim(),
|
||||||
manageGroupMemberships: z.boolean().optional().default(false)
|
manageGroupMemberships: z.boolean().optional().default(false),
|
||||||
|
jwtSignatureAlgorithm: z
|
||||||
|
.nativeEnum(OIDCJWTSignatureAlgorithm)
|
||||||
|
.optional()
|
||||||
|
.default(OIDCJWTSignatureAlgorithm.RS256)
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (data.configurationType === OIDCConfigurationType.CUSTOM) {
|
if (data.configurationType === OIDCConfigurationType.CUSTOM) {
|
||||||
|
@@ -223,12 +223,18 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
samlConfigId: z.string().trim()
|
samlConfigId: z.string().trim()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
preValidation: passport.authenticate("saml", {
|
preValidation: passport.authenticate(
|
||||||
session: false,
|
"saml",
|
||||||
failureFlash: true,
|
{
|
||||||
failureRedirect: "/login/provider/error"
|
session: false
|
||||||
// this is due to zod type difference
|
},
|
||||||
}) as any,
|
async (req, res, err, user) => {
|
||||||
|
if (err) {
|
||||||
|
throw new BadRequestError({ message: `Saml authentication failed. ${err?.message}`, error: err });
|
||||||
|
}
|
||||||
|
req.passportUser = user as { isUserCompleted: boolean; providerAuthToken: string };
|
||||||
|
}
|
||||||
|
) as any, // this is due to zod type difference
|
||||||
handler: (req, res) => {
|
handler: (req, res) => {
|
||||||
if (req.passportUser.isUserCompleted) {
|
if (req.passportUser.isUserCompleted) {
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
|
@@ -23,7 +23,8 @@ export const registerSecretRotationProviderRouter = async (server: FastifyZodPro
|
|||||||
title: z.string(),
|
title: z.string(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
template: z.any()
|
template: z.any(),
|
||||||
|
isDeprecated: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
})
|
})
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas";
|
import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@@ -41,10 +40,16 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async () => {
|
handler: async (req) => {
|
||||||
throw new BadRequestError({
|
const secretRotation = await server.services.secretRotation.createRotation({
|
||||||
message: `This version of Secret Rotations has been deprecated. Please see docs for new version.`
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body,
|
||||||
|
projectId: req.body.workspaceId
|
||||||
});
|
});
|
||||||
|
return { secretRotation };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -0,0 +1,19 @@
|
|||||||
|
import {
|
||||||
|
Auth0ClientSecretRotationGeneratedCredentialsSchema,
|
||||||
|
Auth0ClientSecretRotationSchema,
|
||||||
|
CreateAuth0ClientSecretRotationSchema,
|
||||||
|
UpdateAuth0ClientSecretRotationSchema
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
|
||||||
|
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||||
|
|
||||||
|
export const registerAuth0ClientSecretRotationRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSecretRotationEndpoints({
|
||||||
|
type: SecretRotation.Auth0ClientSecret,
|
||||||
|
server,
|
||||||
|
responseSchema: Auth0ClientSecretRotationSchema,
|
||||||
|
createSchema: CreateAuth0ClientSecretRotationSchema,
|
||||||
|
updateSchema: UpdateAuth0ClientSecretRotationSchema,
|
||||||
|
generatedCredentialsSchema: Auth0ClientSecretRotationGeneratedCredentialsSchema
|
||||||
|
});
|
@@ -1,5 +1,6 @@
|
|||||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
|
||||||
|
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
|
||||||
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
||||||
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
||||||
|
|
||||||
@@ -10,5 +11,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
|||||||
(server: FastifyZodProvider) => Promise<void>
|
(server: FastifyZodProvider) => Promise<void>
|
||||||
> = {
|
> = {
|
||||||
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
|
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
|
||||||
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter
|
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
|
||||||
|
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter
|
||||||
};
|
};
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||||
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||||
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||||
@@ -11,7 +12,8 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
|||||||
|
|
||||||
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
||||||
PostgresCredentialsRotationListItemSchema,
|
PostgresCredentialsRotationListItemSchema,
|
||||||
MsSqlCredentialsRotationListItemSchema
|
MsSqlCredentialsRotationListItemSchema,
|
||||||
|
Auth0ClientSecretRotationListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
||||||
|
@@ -78,10 +78,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan?.dynamicSecret) {
|
if (!plan?.dynamicSecret) {
|
||||||
@@ -102,6 +98,15 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.Lease,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
|
const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
|
||||||
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
|
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
|
||||||
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
|
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
|
||||||
@@ -159,10 +164,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
@@ -187,7 +188,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({
|
||||||
|
id: dynamicSecretLease.dynamicSecretId,
|
||||||
|
folderId: folder.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dynamicSecretCfg)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.Lease,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const decryptedStoredInput = JSON.parse(
|
const decryptedStoredInput = JSON.parse(
|
||||||
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
||||||
@@ -239,10 +258,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
@@ -259,7 +274,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id)
|
if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id)
|
||||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||||
|
|
||||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({
|
||||||
|
id: dynamicSecretLease.dynamicSecretId,
|
||||||
|
folderId: folder.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dynamicSecretCfg)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.Lease,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const decryptedStoredInput = JSON.parse(
|
const decryptedStoredInput = JSON.parse(
|
||||||
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
||||||
@@ -309,10 +342,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
@@ -326,6 +355,15 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.Lease,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
||||||
return dynamicSecretLeases;
|
return dynamicSecretLeases;
|
||||||
};
|
};
|
||||||
@@ -352,10 +390,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' not found` });
|
if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' not found` });
|
||||||
@@ -364,6 +398,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
if (!dynamicSecretLease)
|
if (!dynamicSecretLease)
|
||||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||||
|
|
||||||
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({
|
||||||
|
id: dynamicSecretLease.dynamicSecretId,
|
||||||
|
folderId: folder.id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dynamicSecretCfg)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.Lease,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return dynamicSecretLease;
|
return dynamicSecretLease;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,9 +1,17 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName, TDynamicSecrets } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import {
|
||||||
|
buildFindFilter,
|
||||||
|
ormify,
|
||||||
|
prependTableNameToFindFilter,
|
||||||
|
selectAllTableCols,
|
||||||
|
sqlNestRelationships,
|
||||||
|
TFindFilter,
|
||||||
|
TFindOpt
|
||||||
|
} from "@app/lib/knex";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
@@ -12,6 +20,86 @@ export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory
|
|||||||
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||||
const orm = ormify(db, TableName.DynamicSecret);
|
const orm = ormify(db, TableName.DynamicSecret);
|
||||||
|
|
||||||
|
const findOne = async (filter: TFindFilter<TDynamicSecrets>, tx?: Knex) => {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ResourceMetadata,
|
||||||
|
`${TableName.ResourceMetadata}.dynamicSecretId`,
|
||||||
|
`${TableName.DynamicSecret}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.DynamicSecret))
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||||
|
)
|
||||||
|
.where(prependTableNameToFindFilter(TableName.DynamicSecret, filter));
|
||||||
|
|
||||||
|
const docs = sqlNestRelationships({
|
||||||
|
data: await query,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => el,
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return docs[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const findWithMetadata = async (
|
||||||
|
filter: TFindFilter<TDynamicSecrets>,
|
||||||
|
{ offset, limit, sort, tx }: TFindOpt<TDynamicSecrets> = {}
|
||||||
|
) => {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.DynamicSecret)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ResourceMetadata,
|
||||||
|
`${TableName.ResourceMetadata}.dynamicSecretId`,
|
||||||
|
`${TableName.DynamicSecret}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.DynamicSecret))
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter(filter));
|
||||||
|
|
||||||
|
if (limit) void query.limit(limit);
|
||||||
|
if (offset) void query.offset(offset);
|
||||||
|
if (sort) {
|
||||||
|
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = sqlNestRelationships({
|
||||||
|
data: await query,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => el,
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return docs;
|
||||||
|
};
|
||||||
|
|
||||||
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
|
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
|
||||||
const listDynamicSecretsByFolderIds = async (
|
const listDynamicSecretsByFolderIds = async (
|
||||||
{
|
{
|
||||||
@@ -39,18 +127,27 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
|||||||
void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`);
|
void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ResourceMetadata,
|
||||||
|
`${TableName.ResourceMetadata}.dynamicSecretId`,
|
||||||
|
`${TableName.DynamicSecret}.id`
|
||||||
|
)
|
||||||
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
|
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
|
||||||
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
.select(
|
.select(
|
||||||
selectAllTableCols(TableName.DynamicSecret),
|
selectAllTableCols(TableName.DynamicSecret),
|
||||||
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||||
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`)
|
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`),
|
||||||
|
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||||
|
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||||
|
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||||
)
|
)
|
||||||
.orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection);
|
.orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection);
|
||||||
|
|
||||||
|
let queryWithLimit;
|
||||||
if (limit) {
|
if (limit) {
|
||||||
const rankOffset = offset + 1;
|
const rankOffset = offset + 1;
|
||||||
return await (tx || db)
|
queryWithLimit = (tx || db.replicaNode())
|
||||||
.with("w", query)
|
.with("w", query)
|
||||||
.select("*")
|
.select("*")
|
||||||
.from<Awaited<typeof query>[number]>("w")
|
.from<Awaited<typeof query>[number]>("w")
|
||||||
@@ -58,7 +155,22 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
|||||||
.andWhere("w.rank", "<", rankOffset + limit);
|
.andWhere("w.rank", "<", rankOffset + limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dynamicSecrets = await query;
|
const dynamicSecrets = sqlNestRelationships({
|
||||||
|
data: await (queryWithLimit || query),
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => el,
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "metadataId",
|
||||||
|
label: "metadata" as const,
|
||||||
|
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||||
|
id: metadataId,
|
||||||
|
key: metadataKey,
|
||||||
|
value: metadataValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
return dynamicSecrets;
|
return dynamicSecrets;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -66,5 +178,5 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...orm, listDynamicSecretsByFolderIds };
|
return { ...orm, listDynamicSecretsByFolderIds, findOne, findWithMetadata };
|
||||||
};
|
};
|
||||||
|
@@ -42,7 +42,7 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
|
|||||||
inputHostIps.push(...resolvedIps);
|
inputHostIps.push(...resolvedIps);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isGateway && !appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP) {
|
if (!isGateway && !(appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP || appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)) {
|
||||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||||
if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" });
|
if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" });
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,7 @@ import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
|||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
|
||||||
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||||
@@ -46,6 +47,7 @@ type TDynamicSecretServiceFactoryDep = {
|
|||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
||||||
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||||
@@ -60,7 +62,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
dynamicSecretQueueService,
|
dynamicSecretQueueService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectGatewayDAL
|
projectGatewayDAL,
|
||||||
|
resourceMetadataDAL
|
||||||
}: TDynamicSecretServiceFactoryDep) => {
|
}: TDynamicSecretServiceFactoryDep) => {
|
||||||
const create = async ({
|
const create = async ({
|
||||||
path,
|
path,
|
||||||
@@ -73,7 +76,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
projectSlug,
|
projectSlug,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
actorAuthMethod
|
actorAuthMethod,
|
||||||
|
metadata
|
||||||
}: TCreateDynamicSecretDTO) => {
|
}: TCreateDynamicSecretDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
@@ -87,9 +91,10 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path, metadata })
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
@@ -131,16 +136,36 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
const dynamicSecretCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||||
type: provider.type,
|
const cfg = await dynamicSecretDAL.create(
|
||||||
version: 1,
|
{
|
||||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
|
type: provider.type,
|
||||||
maxTTL,
|
version: 1,
|
||||||
defaultTTL,
|
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
|
||||||
folderId: folder.id,
|
maxTTL,
|
||||||
name,
|
defaultTTL,
|
||||||
projectGatewayId: selectedGatewayId
|
folderId: folder.id,
|
||||||
|
name,
|
||||||
|
projectGatewayId: selectedGatewayId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
await resourceMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
dynamicSecretId: cfg.id,
|
||||||
|
orgId: actorOrgId
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
});
|
});
|
||||||
|
|
||||||
return dynamicSecretCfg;
|
return dynamicSecretCfg;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,7 +181,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
newName,
|
newName,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod,
|
||||||
|
metadata
|
||||||
}: TUpdateDynamicSecretDTO) => {
|
}: TUpdateDynamicSecretDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
@@ -171,10 +197,6 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan?.dynamicSecret) {
|
if (!plan?.dynamicSecret) {
|
||||||
@@ -193,6 +215,27 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found`
|
message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (newName) {
|
if (newName) {
|
||||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
||||||
if (existingDynamicSecret)
|
if (existingDynamicSecret)
|
||||||
@@ -231,14 +274,41 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
|
||||||
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }).cipherTextBlob,
|
const cfg = await dynamicSecretDAL.updateById(
|
||||||
maxTTL,
|
dynamicSecretCfg.id,
|
||||||
defaultTTL,
|
{
|
||||||
name: newName ?? name,
|
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) })
|
||||||
status: null,
|
.cipherTextBlob,
|
||||||
statusDetails: null,
|
maxTTL,
|
||||||
projectGatewayId: selectedGatewayId
|
defaultTTL,
|
||||||
|
name: newName ?? name,
|
||||||
|
status: null,
|
||||||
|
projectGatewayId: selectedGatewayId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
await resourceMetadataDAL.delete(
|
||||||
|
{
|
||||||
|
dynamicSecretId: cfg.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await resourceMetadataDAL.insertMany(
|
||||||
|
metadata.map(({ key, value }) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
dynamicSecretId: cfg.id,
|
||||||
|
orgId: actorOrgId
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedDynamicCfg;
|
return updatedDynamicCfg;
|
||||||
@@ -268,10 +338,6 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
@@ -282,6 +348,15 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` });
|
throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
||||||
// when not forced we check with the external system to first remove the things
|
// when not forced we check with the external system to first remove the things
|
||||||
// we introduce a forced concept because consider the external lease got deleted by some other external like a human or another system
|
// we introduce a forced concept because consider the external lease got deleted by some other external like a human or another system
|
||||||
@@ -329,14 +404,6 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
@@ -346,6 +413,25 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
if (!dynamicSecretCfg) {
|
if (!dynamicSecretCfg) {
|
||||||
throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` });
|
throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecretCfg.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId
|
projectId
|
||||||
@@ -356,6 +442,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
) as object;
|
) as object;
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
||||||
|
|
||||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -426,7 +513,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
ProjectPermissionSub.DynamicSecrets
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
@@ -473,16 +560,12 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` });
|
throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` });
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.find(
|
const dynamicSecretCfg = await dynamicSecretDAL.findWithMetadata(
|
||||||
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
||||||
{
|
{
|
||||||
limit,
|
limit,
|
||||||
@@ -490,7 +573,17 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
sort: orderBy ? [[orderBy, orderDirection]] : undefined
|
sort: orderBy ? [[orderBy, orderDirection]] : undefined
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return dynamicSecretCfg;
|
|
||||||
|
return dynamicSecretCfg.filter((dynamicSecret) => {
|
||||||
|
return permission.can(
|
||||||
|
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: environmentSlug,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecret.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const listDynamicSecretsByFolderIds = async (
|
const listDynamicSecretsByFolderIds = async (
|
||||||
@@ -542,24 +635,14 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
isInternal,
|
isInternal,
|
||||||
...params
|
...params
|
||||||
}: TListDynamicSecretsMultiEnvDTO) => {
|
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||||
if (!isInternal) {
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
actor,
|
||||||
actor,
|
actorId,
|
||||||
actorId,
|
projectId,
|
||||||
projectId,
|
actorAuthMethod,
|
||||||
actorAuthMethod,
|
actorOrgId,
|
||||||
actorOrgId,
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// verify user has access to each env in request
|
|
||||||
environmentSlugs.forEach((environmentSlug) =>
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||||
if (!folders.length)
|
if (!folders.length)
|
||||||
@@ -572,7 +655,16 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
...params
|
...params
|
||||||
});
|
});
|
||||||
|
|
||||||
return dynamicSecretCfg;
|
return dynamicSecretCfg.filter((dynamicSecret) => {
|
||||||
|
return permission.can(
|
||||||
|
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||||
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment: dynamicSecret.environment,
|
||||||
|
secretPath: path,
|
||||||
|
metadata: dynamicSecret.metadata
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchAzureEntraIdUsers = async ({
|
const fetchAzureEntraIdUsers = async ({
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||||
|
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
import { DynamicSecretProviderSchema } from "./providers/models";
|
import { DynamicSecretProviderSchema } from "./providers/models";
|
||||||
@@ -20,6 +21,7 @@ export type TCreateDynamicSecretDTO = {
|
|||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
name: string;
|
name: string;
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
metadata?: ResourceMetadataDTO;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateDynamicSecretDTO = {
|
export type TUpdateDynamicSecretDTO = {
|
||||||
@@ -31,6 +33,7 @@ export type TUpdateDynamicSecretDTO = {
|
|||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
inputs?: TProvider["inputs"];
|
inputs?: TProvider["inputs"];
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
metadata?: ResourceMetadataDTO;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TDeleteDynamicSecretDTO = {
|
export type TDeleteDynamicSecretDTO = {
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
||||||
@@ -8,22 +7,5 @@ export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
|
|||||||
export const oidcConfigDALFactory = (db: TDbClient) => {
|
export const oidcConfigDALFactory = (db: TDbClient) => {
|
||||||
const oidcCfgOrm = ormify(db, TableName.OidcConfig);
|
const oidcCfgOrm = ormify(db, TableName.OidcConfig);
|
||||||
|
|
||||||
const findEnforceableOidcCfg = async (orgId: string) => {
|
return oidcCfgOrm;
|
||||||
try {
|
|
||||||
const oidcCfg = await db
|
|
||||||
.replicaNode()(TableName.OidcConfig)
|
|
||||||
.where({
|
|
||||||
orgId,
|
|
||||||
isActive: true
|
|
||||||
})
|
|
||||||
.whereNotNull("lastUsed")
|
|
||||||
.first();
|
|
||||||
|
|
||||||
return oidcCfg;
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "Find org by id" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return { ...oidcCfgOrm, findEnforceableOidcCfg };
|
|
||||||
};
|
};
|
||||||
|
@@ -165,7 +165,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
allowedEmailDomains: oidcCfg.allowedEmailDomains,
|
allowedEmailDomains: oidcCfg.allowedEmailDomains,
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
manageGroupMemberships: oidcCfg.manageGroupMemberships,
|
||||||
|
jwtSignatureAlgorithm: oidcCfg.jwtSignatureAlgorithm
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -481,7 +482,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
userinfoEndpoint,
|
userinfoEndpoint,
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
manageGroupMemberships
|
manageGroupMemberships,
|
||||||
|
jwtSignatureAlgorithm
|
||||||
}: TUpdateOidcCfgDTO) => {
|
}: TUpdateOidcCfgDTO) => {
|
||||||
const org = await orgDAL.findOne({
|
const org = await orgDAL.findOne({
|
||||||
slug: orgSlug
|
slug: orgSlug
|
||||||
@@ -536,7 +538,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
jwksUri,
|
jwksUri,
|
||||||
isActive,
|
isActive,
|
||||||
lastUsed: null,
|
lastUsed: null,
|
||||||
manageGroupMemberships
|
manageGroupMemberships,
|
||||||
|
jwtSignatureAlgorithm
|
||||||
};
|
};
|
||||||
|
|
||||||
if (clientId !== undefined) {
|
if (clientId !== undefined) {
|
||||||
@@ -569,7 +572,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
userinfoEndpoint,
|
userinfoEndpoint,
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
manageGroupMemberships
|
manageGroupMemberships,
|
||||||
|
jwtSignatureAlgorithm
|
||||||
}: TCreateOidcCfgDTO) => {
|
}: TCreateOidcCfgDTO) => {
|
||||||
const org = await orgDAL.findOne({
|
const org = await orgDAL.findOne({
|
||||||
slug: orgSlug
|
slug: orgSlug
|
||||||
@@ -613,6 +617,7 @@ export const oidcConfigServiceFactory = ({
|
|||||||
userinfoEndpoint,
|
userinfoEndpoint,
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
manageGroupMemberships,
|
manageGroupMemberships,
|
||||||
|
jwtSignatureAlgorithm,
|
||||||
encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob,
|
encryptedOidcClientId: encryptor({ plainText: Buffer.from(clientId) }).cipherTextBlob,
|
||||||
encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob
|
encryptedOidcClientSecret: encryptor({ plainText: Buffer.from(clientSecret) }).cipherTextBlob
|
||||||
});
|
});
|
||||||
@@ -676,7 +681,8 @@ export const oidcConfigServiceFactory = ({
|
|||||||
const client = new issuer.Client({
|
const client = new issuer.Client({
|
||||||
client_id: oidcCfg.clientId,
|
client_id: oidcCfg.clientId,
|
||||||
client_secret: oidcCfg.clientSecret,
|
client_secret: oidcCfg.clientSecret,
|
||||||
redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`]
|
redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`],
|
||||||
|
id_token_signed_response_alg: oidcCfg.jwtSignatureAlgorithm
|
||||||
});
|
});
|
||||||
|
|
||||||
const strategy = new OpenIdStrategy(
|
const strategy = new OpenIdStrategy(
|
||||||
|
@@ -5,6 +5,12 @@ export enum OIDCConfigurationType {
|
|||||||
DISCOVERY_URL = "discoveryURL"
|
DISCOVERY_URL = "discoveryURL"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OIDCJWTSignatureAlgorithm {
|
||||||
|
RS256 = "RS256",
|
||||||
|
HS256 = "HS256",
|
||||||
|
RS512 = "RS512"
|
||||||
|
}
|
||||||
|
|
||||||
export type TOidcLoginDTO = {
|
export type TOidcLoginDTO = {
|
||||||
externalId: string;
|
externalId: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -40,6 +46,7 @@ export type TCreateOidcCfgDTO = {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
orgSlug: string;
|
orgSlug: string;
|
||||||
manageGroupMemberships: boolean;
|
manageGroupMemberships: boolean;
|
||||||
|
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
|
||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
|
|
||||||
export type TUpdateOidcCfgDTO = Partial<{
|
export type TUpdateOidcCfgDTO = Partial<{
|
||||||
@@ -56,5 +63,6 @@ export type TUpdateOidcCfgDTO = Partial<{
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
orgSlug: string;
|
orgSlug: string;
|
||||||
manageGroupMemberships: boolean;
|
manageGroupMemberships: boolean;
|
||||||
|
jwtSignatureAlgorithm: OIDCJWTSignatureAlgorithm;
|
||||||
}> &
|
}> &
|
||||||
TGenericPermission;
|
TGenericPermission;
|
||||||
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import {
|
import {
|
||||||
IdentityProjectMembershipRoleSchema,
|
IdentityProjectMembershipRoleSchema,
|
||||||
|
OrgMembershipRole,
|
||||||
OrgMembershipsSchema,
|
OrgMembershipsSchema,
|
||||||
TableName,
|
TableName,
|
||||||
TProjectRoles,
|
TProjectRoles,
|
||||||
@@ -53,6 +54,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
|
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
|
||||||
db.ref("permissions").withSchema(TableName.OrgRoles),
|
db.ref("permissions").withSchema(TableName.OrgRoles),
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
|
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
||||||
db.ref("groupId").withSchema("userGroups"),
|
db.ref("groupId").withSchema("userGroups"),
|
||||||
db.ref("groupOrgId").withSchema("userGroups"),
|
db.ref("groupOrgId").withSchema("userGroups"),
|
||||||
db.ref("groupName").withSchema("userGroups"),
|
db.ref("groupName").withSchema("userGroups"),
|
||||||
@@ -71,6 +73,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
OrgMembershipsSchema.extend({
|
OrgMembershipsSchema.extend({
|
||||||
permissions: z.unknown(),
|
permissions: z.unknown(),
|
||||||
orgAuthEnforced: z.boolean().optional().nullable(),
|
orgAuthEnforced: z.boolean().optional().nullable(),
|
||||||
|
bypassOrgAuthEnabled: z.boolean(),
|
||||||
customRoleSlug: z.string().optional().nullable(),
|
customRoleSlug: z.string().optional().nullable(),
|
||||||
shouldUseNewPrivilegeSystem: z.boolean()
|
shouldUseNewPrivilegeSystem: z.boolean()
|
||||||
}).parse(el),
|
}).parse(el),
|
||||||
@@ -571,6 +574,11 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
})
|
})
|
||||||
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
|
.join<TProjects>(TableName.Project, `${TableName.Project}.id`, db.raw("?", [projectId]))
|
||||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
|
.join(TableName.OrgMembership, (qb) => {
|
||||||
|
void qb
|
||||||
|
.on(`${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.andOn(`${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`);
|
||||||
|
})
|
||||||
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||||
void queryBuilder
|
void queryBuilder
|
||||||
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
|
.on(`${TableName.Users}.id`, `${TableName.IdentityMetadata}.userId`)
|
||||||
@@ -670,6 +678,8 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||||
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
||||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||||
|
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
||||||
|
db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"),
|
||||||
db.ref("orgId").withSchema(TableName.Project),
|
db.ref("orgId").withSchema(TableName.Project),
|
||||||
db.ref("type").withSchema(TableName.Project).as("projectType"),
|
db.ref("type").withSchema(TableName.Project).as("projectType"),
|
||||||
db.ref("id").withSchema(TableName.Project).as("projectId"),
|
db.ref("id").withSchema(TableName.Project).as("projectId"),
|
||||||
@@ -683,6 +693,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
orgId,
|
orgId,
|
||||||
username,
|
username,
|
||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
|
orgRole,
|
||||||
membershipId,
|
membershipId,
|
||||||
groupMembershipId,
|
groupMembershipId,
|
||||||
membershipCreatedAt,
|
membershipCreatedAt,
|
||||||
@@ -690,10 +701,12 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
groupMembershipUpdatedAt,
|
groupMembershipUpdatedAt,
|
||||||
membershipUpdatedAt,
|
membershipUpdatedAt,
|
||||||
projectType,
|
projectType,
|
||||||
shouldUseNewPrivilegeSystem
|
shouldUseNewPrivilegeSystem,
|
||||||
|
bypassOrgAuthEnabled
|
||||||
}) => ({
|
}) => ({
|
||||||
orgId,
|
orgId,
|
||||||
orgAuthEnforced,
|
orgAuthEnforced,
|
||||||
|
orgRole: orgRole as OrgMembershipRole,
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
username,
|
username,
|
||||||
@@ -701,7 +714,8 @@ export const permissionDALFactory = (db: TDbClient) => {
|
|||||||
id: membershipId || groupMembershipId,
|
id: membershipId || groupMembershipId,
|
||||||
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||||
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt,
|
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt,
|
||||||
shouldUseNewPrivilegeSystem
|
shouldUseNewPrivilegeSystem,
|
||||||
|
bypassOrgAuthEnabled
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability";
|
import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { TOrganizations } from "@app/db/schemas";
|
import { OrgMembershipRole, TOrganizations } from "@app/db/schemas";
|
||||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||||
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
|
||||||
@@ -118,11 +118,20 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
|||||||
].includes(actorAuthMethod);
|
].includes(actorAuthMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrganizations["authEnforced"]) {
|
function validateOrgSSO(
|
||||||
|
actorAuthMethod: ActorAuthMethod,
|
||||||
|
isOrgSsoEnforced: TOrganizations["authEnforced"],
|
||||||
|
isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"],
|
||||||
|
orgRole: OrgMembershipRole
|
||||||
|
) {
|
||||||
if (actorAuthMethod === undefined) {
|
if (actorAuthMethod === undefined) {
|
||||||
throw new UnauthorizedError({ name: "No auth method defined" });
|
throw new UnauthorizedError({ name: "No auth method defined" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isOrgSsoEnforced && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isOrgSsoEnforced &&
|
isOrgSsoEnforced &&
|
||||||
actorAuthMethod !== null &&
|
actorAuthMethod !== null &&
|
||||||
|
@@ -139,7 +139,12 @@ export const permissionServiceFactory = ({
|
|||||||
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
||||||
}
|
}
|
||||||
|
|
||||||
validateOrgSSO(authMethod, membership.orgAuthEnforced);
|
validateOrgSSO(
|
||||||
|
authMethod,
|
||||||
|
membership.orgAuthEnforced,
|
||||||
|
membership.bypassOrgAuthEnabled,
|
||||||
|
membership.role as OrgMembershipRole
|
||||||
|
);
|
||||||
|
|
||||||
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat(
|
const finalPolicyRoles = [{ role: membership.role, permissions: membership.permissions }].concat(
|
||||||
membership?.groups?.map(({ role, customRolePermission }) => ({
|
membership?.groups?.map(({ role, customRolePermission }) => ({
|
||||||
@@ -226,7 +231,12 @@ export const permissionServiceFactory = ({
|
|||||||
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
||||||
}
|
}
|
||||||
|
|
||||||
validateOrgSSO(authMethod, userProjectPermission.orgAuthEnforced);
|
validateOrgSSO(
|
||||||
|
authMethod,
|
||||||
|
userProjectPermission.orgAuthEnforced,
|
||||||
|
userProjectPermission.bypassOrgAuthEnabled,
|
||||||
|
userProjectPermission.orgRole
|
||||||
|
);
|
||||||
|
|
||||||
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) {
|
if (actionProjectType !== ActionProjectType.Any && actionProjectType !== userProjectPermission.projectType) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
|
@@ -155,6 +155,10 @@ export type SecretFolderSubjectFields = {
|
|||||||
export type DynamicSecretSubjectFields = {
|
export type DynamicSecretSubjectFields = {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
|
metadata?: {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SecretImportSubjectFields = {
|
export type SecretImportSubjectFields = {
|
||||||
@@ -284,6 +288,42 @@ const SecretConditionV1Schema = z
|
|||||||
})
|
})
|
||||||
.partial();
|
.partial();
|
||||||
|
|
||||||
|
const DynamicSecretConditionV2Schema = z
|
||||||
|
.object({
|
||||||
|
environment: z.union([
|
||||||
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||||
|
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||||
|
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
]),
|
||||||
|
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA,
|
||||||
|
metadata: z.object({
|
||||||
|
[PermissionConditionOperators.$ELEMENTMATCH]: z
|
||||||
|
.object({
|
||||||
|
key: z
|
||||||
|
.object({
|
||||||
|
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||||
|
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||||
|
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||||
|
})
|
||||||
|
.partial(),
|
||||||
|
value: z
|
||||||
|
.object({
|
||||||
|
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||||
|
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||||
|
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
const SecretConditionV2Schema = z
|
const SecretConditionV2Schema = z
|
||||||
.object({
|
.object({
|
||||||
environment: z.union([
|
environment: z.union([
|
||||||
@@ -581,7 +621,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
|||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
),
|
),
|
||||||
conditions: SecretConditionV1Schema.describe(
|
conditions: DynamicSecretConditionV2Schema.describe(
|
||||||
"When specified, only matching conditions will be allowed to access given resource."
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
).optional()
|
).optional()
|
||||||
}),
|
}),
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName } from "@app/db/schemas";
|
import { TableName } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
|
||||||
import { ormify } from "@app/lib/knex";
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TSamlConfigDALFactory = ReturnType<typeof samlConfigDALFactory>;
|
export type TSamlConfigDALFactory = ReturnType<typeof samlConfigDALFactory>;
|
||||||
@@ -8,25 +7,5 @@ export type TSamlConfigDALFactory = ReturnType<typeof samlConfigDALFactory>;
|
|||||||
export const samlConfigDALFactory = (db: TDbClient) => {
|
export const samlConfigDALFactory = (db: TDbClient) => {
|
||||||
const samlCfgOrm = ormify(db, TableName.SamlConfig);
|
const samlCfgOrm = ormify(db, TableName.SamlConfig);
|
||||||
|
|
||||||
const findEnforceableSamlCfg = async (orgId: string) => {
|
return samlCfgOrm;
|
||||||
try {
|
|
||||||
const samlCfg = await db
|
|
||||||
.replicaNode()(TableName.SamlConfig)
|
|
||||||
.where({
|
|
||||||
orgId,
|
|
||||||
isActive: true
|
|
||||||
})
|
|
||||||
.whereNotNull("lastUsed")
|
|
||||||
.first();
|
|
||||||
|
|
||||||
return samlCfg;
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "Find org by id" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...samlCfgOrm,
|
|
||||||
findEnforceableSamlCfg
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||||
|
name: "Auth0 Client Secret",
|
||||||
|
type: SecretRotation.Auth0ClientSecret,
|
||||||
|
connection: AppConnection.Auth0,
|
||||||
|
template: {
|
||||||
|
secretsMapping: {
|
||||||
|
clientId: "AUTH0_CLIENT_ID",
|
||||||
|
clientSecret: "AUTH0_CLIENT_SECRET"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,104 @@
|
|||||||
|
import {
|
||||||
|
TAuth0ClientSecretRotationGeneratedCredentials,
|
||||||
|
TAuth0ClientSecretRotationWithConnection
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-types";
|
||||||
|
import {
|
||||||
|
TRotationFactory,
|
||||||
|
TRotationFactoryGetSecretsPayload,
|
||||||
|
TRotationFactoryIssueCredentials,
|
||||||
|
TRotationFactoryRevokeCredentials,
|
||||||
|
TRotationFactoryRotateCredentials
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
|
import { getAuth0ConnectionAccessToken } from "@app/services/app-connection/auth0";
|
||||||
|
|
||||||
|
import { generatePassword } from "../shared/utils";
|
||||||
|
|
||||||
|
export const auth0ClientSecretRotationFactory: TRotationFactory<
|
||||||
|
TAuth0ClientSecretRotationWithConnection,
|
||||||
|
TAuth0ClientSecretRotationGeneratedCredentials
|
||||||
|
> = (secretRotation, appConnectionDAL, kmsService) => {
|
||||||
|
const {
|
||||||
|
connection,
|
||||||
|
parameters: { clientId },
|
||||||
|
secretsMapping
|
||||||
|
} = secretRotation;
|
||||||
|
|
||||||
|
const $rotateClientSecret = async () => {
|
||||||
|
const accessToken = await getAuth0ConnectionAccessToken(connection, appConnectionDAL, kmsService);
|
||||||
|
const { audience } = connection.credentials;
|
||||||
|
await blockLocalAndPrivateIpAddresses(audience);
|
||||||
|
const clientSecret = generatePassword();
|
||||||
|
|
||||||
|
await request.request({
|
||||||
|
method: "PATCH",
|
||||||
|
url: `${audience}clients/${clientId}`,
|
||||||
|
headers: { authorization: `Bearer ${accessToken}` },
|
||||||
|
data: {
|
||||||
|
client_secret: clientSecret
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { clientId, clientSecret };
|
||||||
|
};
|
||||||
|
|
||||||
|
const issueCredentials: TRotationFactoryIssueCredentials<TAuth0ClientSecretRotationGeneratedCredentials> = async (
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
const credentials = await $rotateClientSecret();
|
||||||
|
|
||||||
|
return callback(credentials);
|
||||||
|
};
|
||||||
|
|
||||||
|
const revokeCredentials: TRotationFactoryRevokeCredentials<TAuth0ClientSecretRotationGeneratedCredentials> = async (
|
||||||
|
_,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
const accessToken = await getAuth0ConnectionAccessToken(connection, appConnectionDAL, kmsService);
|
||||||
|
const { audience } = connection.credentials;
|
||||||
|
await blockLocalAndPrivateIpAddresses(audience);
|
||||||
|
|
||||||
|
// we just trigger an auth0 rotation to negate our credentials
|
||||||
|
await request.request({
|
||||||
|
method: "POST",
|
||||||
|
url: `${audience}clients/${clientId}/rotate-secret`,
|
||||||
|
headers: { authorization: `Bearer ${accessToken}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
return callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotateCredentials: TRotationFactoryRotateCredentials<TAuth0ClientSecretRotationGeneratedCredentials> = async (
|
||||||
|
_,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
const credentials = await $rotateClientSecret();
|
||||||
|
|
||||||
|
return callback(credentials);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TAuth0ClientSecretRotationGeneratedCredentials> = (
|
||||||
|
generatedCredentials
|
||||||
|
) => {
|
||||||
|
const secrets = [
|
||||||
|
{
|
||||||
|
key: secretsMapping.clientId,
|
||||||
|
value: generatedCredentials.clientId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: secretsMapping.clientSecret,
|
||||||
|
value: generatedCredentials.clientSecret
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
issueCredentials,
|
||||||
|
revokeCredentials,
|
||||||
|
rotateCredentials,
|
||||||
|
getSecretsPayload
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,67 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
import {
|
||||||
|
BaseCreateSecretRotationSchema,
|
||||||
|
BaseSecretRotationSchema,
|
||||||
|
BaseUpdateSecretRotationSchema
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||||
|
import { SecretRotations } from "@app/lib/api-docs";
|
||||||
|
import { SecretNameSchema } from "@app/server/lib/schemas";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
export const Auth0ClientSecretRotationGeneratedCredentialsSchema = z
|
||||||
|
.object({
|
||||||
|
clientId: z.string(),
|
||||||
|
clientSecret: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.max(2);
|
||||||
|
|
||||||
|
const Auth0ClientSecretRotationParametersSchema = z.object({
|
||||||
|
clientId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Client ID Required")
|
||||||
|
.describe(SecretRotations.PARAMETERS.AUTH0_CLIENT_SECRET.clientId)
|
||||||
|
});
|
||||||
|
|
||||||
|
const Auth0ClientSecretRotationSecretsMappingSchema = z.object({
|
||||||
|
clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientId),
|
||||||
|
clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AUTH0_CLIENT_SECRET.clientSecret)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Auth0ClientSecretRotationTemplateSchema = z.object({
|
||||||
|
secretsMapping: z.object({
|
||||||
|
clientId: z.string(),
|
||||||
|
clientSecret: z.string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Auth0ClientSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.Auth0ClientSecret).extend({
|
||||||
|
type: z.literal(SecretRotation.Auth0ClientSecret),
|
||||||
|
parameters: Auth0ClientSecretRotationParametersSchema,
|
||||||
|
secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateAuth0ClientSecretRotationSchema = BaseCreateSecretRotationSchema(
|
||||||
|
SecretRotation.Auth0ClientSecret
|
||||||
|
).extend({
|
||||||
|
parameters: Auth0ClientSecretRotationParametersSchema,
|
||||||
|
secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateAuth0ClientSecretRotationSchema = BaseUpdateSecretRotationSchema(
|
||||||
|
SecretRotation.Auth0ClientSecret
|
||||||
|
).extend({
|
||||||
|
parameters: Auth0ClientSecretRotationParametersSchema.optional(),
|
||||||
|
secretsMapping: Auth0ClientSecretRotationSecretsMappingSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Auth0ClientSecretRotationListItemSchema = z.object({
|
||||||
|
name: z.literal("Auth0 Client Secret"),
|
||||||
|
connection: z.literal(AppConnection.Auth0),
|
||||||
|
type: z.literal(SecretRotation.Auth0ClientSecret),
|
||||||
|
template: Auth0ClientSecretRotationTemplateSchema
|
||||||
|
});
|
@@ -0,0 +1,24 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TAuth0Connection } from "@app/services/app-connection/auth0";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Auth0ClientSecretRotationGeneratedCredentialsSchema,
|
||||||
|
Auth0ClientSecretRotationListItemSchema,
|
||||||
|
Auth0ClientSecretRotationSchema,
|
||||||
|
CreateAuth0ClientSecretRotationSchema
|
||||||
|
} from "./auth0-client-secret-rotation-schemas";
|
||||||
|
|
||||||
|
export type TAuth0ClientSecretRotation = z.infer<typeof Auth0ClientSecretRotationSchema>;
|
||||||
|
|
||||||
|
export type TAuth0ClientSecretRotationInput = z.infer<typeof CreateAuth0ClientSecretRotationSchema>;
|
||||||
|
|
||||||
|
export type TAuth0ClientSecretRotationListItem = z.infer<typeof Auth0ClientSecretRotationListItemSchema>;
|
||||||
|
|
||||||
|
export type TAuth0ClientSecretRotationWithConnection = TAuth0ClientSecretRotation & {
|
||||||
|
connection: TAuth0Connection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAuth0ClientSecretRotationGeneratedCredentials = z.infer<
|
||||||
|
typeof Auth0ClientSecretRotationGeneratedCredentialsSchema
|
||||||
|
>;
|
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./auth0-client-secret-rotation-constants";
|
||||||
|
export * from "./auth0-client-secret-rotation-schemas";
|
||||||
|
export * from "./auth0-client-secret-rotation-types";
|
@@ -1,6 +1,7 @@
|
|||||||
export enum SecretRotation {
|
export enum SecretRotation {
|
||||||
PostgresCredentials = "postgres-credentials",
|
PostgresCredentials = "postgres-credentials",
|
||||||
MsSqlCredentials = "mssql-credentials"
|
MsSqlCredentials = "mssql-credentials",
|
||||||
|
Auth0ClientSecret = "auth0-client-secret"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecretRotationStatus {
|
export enum SecretRotationStatus {
|
||||||
|
@@ -3,6 +3,7 @@ import { AxiosError } from "axios";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
|
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
|
||||||
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
||||||
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
||||||
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
|
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
|
||||||
@@ -16,7 +17,8 @@ import {
|
|||||||
|
|
||||||
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
|
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
|
||||||
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||||
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION
|
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||||
|
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listSecretRotationOptions = () => {
|
export const listSecretRotationOptions = () => {
|
||||||
|
@@ -3,10 +3,12 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
|||||||
|
|
||||||
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||||
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
||||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials"
|
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials",
|
||||||
|
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql
|
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
|
||||||
|
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0
|
||||||
};
|
};
|
||||||
|
@@ -13,6 +13,7 @@ import {
|
|||||||
ProjectPermissionSecretRotationActions,
|
ProjectPermissionSecretRotationActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
|
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
|
||||||
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
import {
|
import {
|
||||||
calculateNextRotationAt,
|
calculateNextRotationAt,
|
||||||
@@ -41,6 +42,7 @@ import {
|
|||||||
TRotationFactory,
|
TRotationFactory,
|
||||||
TSecretRotationRotateGeneratedCredentials,
|
TSecretRotationRotateGeneratedCredentials,
|
||||||
TSecretRotationV2,
|
TSecretRotationV2,
|
||||||
|
TSecretRotationV2GeneratedCredentials,
|
||||||
TSecretRotationV2Raw,
|
TSecretRotationV2Raw,
|
||||||
TSecretRotationV2WithConnection,
|
TSecretRotationV2WithConnection,
|
||||||
TUpdateSecretRotationV2DTO
|
TUpdateSecretRotationV2DTO
|
||||||
@@ -53,6 +55,7 @@ import { DatabaseErrorCode } from "@app/lib/error-codes";
|
|||||||
import { BadRequestError, DatabaseError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
||||||
import { QueueJobs, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
||||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
@@ -97,15 +100,21 @@ export type TSecretRotationV2ServiceFactoryDep = {
|
|||||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
queueService: Pick<TQueueServiceFactory, "queuePg">;
|
queueService: Pick<TQueueServiceFactory, "queuePg">;
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
||||||
|
|
||||||
const MAX_GENERATED_CREDENTIALS_LENGTH = 2;
|
const MAX_GENERATED_CREDENTIALS_LENGTH = 2;
|
||||||
|
|
||||||
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactory> = {
|
type TRotationFactoryImplementation = TRotationFactory<
|
||||||
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory,
|
TSecretRotationV2WithConnection,
|
||||||
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory
|
TSecretRotationV2GeneratedCredentials
|
||||||
|
>;
|
||||||
|
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
|
||||||
|
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||||
|
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||||
|
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation
|
||||||
};
|
};
|
||||||
|
|
||||||
export const secretRotationV2ServiceFactory = ({
|
export const secretRotationV2ServiceFactory = ({
|
||||||
@@ -125,7 +134,8 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
secretQueueService,
|
secretQueueService,
|
||||||
snapshotService,
|
snapshotService,
|
||||||
keyStore,
|
keyStore,
|
||||||
queueService
|
queueService,
|
||||||
|
appConnectionDAL
|
||||||
}: TSecretRotationV2ServiceFactoryDep) => {
|
}: TSecretRotationV2ServiceFactoryDep) => {
|
||||||
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
|
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -429,11 +439,15 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
// validates permission to connect and app is valid for rotation type
|
// validates permission to connect and app is valid for rotation type
|
||||||
const connection = await appConnectionService.connectAppConnectionById(typeApp, payload.connectionId, actor);
|
const connection = await appConnectionService.connectAppConnectionById(typeApp, payload.connectionId, actor);
|
||||||
|
|
||||||
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type]({
|
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[payload.type](
|
||||||
parameters: payload.parameters,
|
{
|
||||||
secretsMapping,
|
parameters: payload.parameters,
|
||||||
connection
|
secretsMapping,
|
||||||
} as TSecretRotationV2WithConnection);
|
connection
|
||||||
|
} as TSecretRotationV2WithConnection,
|
||||||
|
appConnectionDAL,
|
||||||
|
kmsService
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
@@ -441,7 +455,7 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
// callback structure to support transactional rollback when possible
|
// callback structure to support transactional rollback when possible
|
||||||
const secretRotation = await rotationFactory.issueCredentials(async (newCredentials) => {
|
const secretRotation = await rotationFactory.issueCredentials(async (newCredentials) => {
|
||||||
const encryptedGeneratedCredentials = await encryptSecretRotationCredentials({
|
const encryptedGeneratedCredentials = await encryptSecretRotationCredentials({
|
||||||
generatedCredentials: [newCredentials],
|
generatedCredentials: [newCredentials] as TSecretRotationV2GeneratedCredentials,
|
||||||
projectId,
|
projectId,
|
||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
@@ -740,32 +754,37 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
message: `Secret Rotation with ID "${rotationId}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteTransaction = secretRotationV2DAL.transaction(async (tx) => {
|
const deleteTransaction = async () =>
|
||||||
if (deleteSecrets) {
|
secretRotationV2DAL.transaction(async (tx) => {
|
||||||
await fnSecretBulkDelete({
|
if (deleteSecrets) {
|
||||||
secretDAL: secretV2BridgeDAL,
|
await fnSecretBulkDelete({
|
||||||
secretQueueService,
|
secretDAL: secretV2BridgeDAL,
|
||||||
inputSecrets: Object.values(secretsMapping as TSecretRotationV2["secretsMapping"]).map((secretKey) => ({
|
secretQueueService,
|
||||||
secretKey,
|
inputSecrets: Object.values(secretsMapping as TSecretRotationV2["secretsMapping"]).map((secretKey) => ({
|
||||||
type: SecretType.Shared
|
secretKey,
|
||||||
})),
|
type: SecretType.Shared
|
||||||
projectId,
|
})),
|
||||||
folderId,
|
projectId,
|
||||||
actorId: actor.id, // not actually used since rotated secrets are shared
|
folderId,
|
||||||
tx
|
actorId: actor.id, // not actually used since rotated secrets are shared
|
||||||
});
|
tx
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return secretRotationV2DAL.deleteById(rotationId, tx);
|
return secretRotationV2DAL.deleteById(rotationId, tx);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (revokeGeneratedCredentials) {
|
if (revokeGeneratedCredentials) {
|
||||||
const appConnection = await decryptAppConnection(connection, kmsService);
|
const appConnection = await decryptAppConnection(connection, kmsService);
|
||||||
|
|
||||||
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type]({
|
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type](
|
||||||
...secretRotation,
|
{
|
||||||
connection: appConnection
|
...secretRotation,
|
||||||
} as TSecretRotationV2WithConnection);
|
connection: appConnection
|
||||||
|
} as TSecretRotationV2WithConnection,
|
||||||
|
appConnectionDAL,
|
||||||
|
kmsService
|
||||||
|
);
|
||||||
|
|
||||||
const generatedCredentials = await decryptSecretRotationCredentials({
|
const generatedCredentials = await decryptSecretRotationCredentials({
|
||||||
encryptedGeneratedCredentials,
|
encryptedGeneratedCredentials,
|
||||||
@@ -773,9 +792,9 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
await rotationFactory.revokeCredentials(generatedCredentials, async () => deleteTransaction);
|
await rotationFactory.revokeCredentials(generatedCredentials, deleteTransaction);
|
||||||
} else {
|
} else {
|
||||||
await deleteTransaction;
|
await deleteTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteSecrets) {
|
if (deleteSecrets) {
|
||||||
@@ -840,10 +859,14 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
|
|
||||||
const inactiveCredentials = generatedCredentials[inactiveIndex];
|
const inactiveCredentials = generatedCredentials[inactiveIndex];
|
||||||
|
|
||||||
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation]({
|
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation](
|
||||||
...secretRotation,
|
{
|
||||||
connection: appConnection
|
...secretRotation,
|
||||||
} as TSecretRotationV2WithConnection);
|
connection: appConnection
|
||||||
|
} as TSecretRotationV2WithConnection,
|
||||||
|
appConnectionDAL,
|
||||||
|
kmsService
|
||||||
|
);
|
||||||
|
|
||||||
const updatedRotation = await rotationFactory.rotateCredentials(inactiveCredentials, async (newCredentials) => {
|
const updatedRotation = await rotationFactory.rotateCredentials(inactiveCredentials, async (newCredentials) => {
|
||||||
const updatedCredentials = [...generatedCredentials];
|
const updatedCredentials = [...generatedCredentials];
|
||||||
@@ -851,7 +874,7 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
|
|
||||||
const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({
|
const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({
|
||||||
projectId,
|
projectId,
|
||||||
generatedCredentials: updatedCredentials,
|
generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials,
|
||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,8 +1,17 @@
|
|||||||
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
|
import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TAuth0ClientSecretRotation,
|
||||||
|
TAuth0ClientSecretRotationGeneratedCredentials,
|
||||||
|
TAuth0ClientSecretRotationInput,
|
||||||
|
TAuth0ClientSecretRotationListItem,
|
||||||
|
TAuth0ClientSecretRotationWithConnection
|
||||||
|
} from "./auth0-client-secret";
|
||||||
import {
|
import {
|
||||||
TMsSqlCredentialsRotation,
|
TMsSqlCredentialsRotation,
|
||||||
TMsSqlCredentialsRotationInput,
|
TMsSqlCredentialsRotationInput,
|
||||||
@@ -18,17 +27,26 @@ import {
|
|||||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||||
import { SecretRotation } from "./secret-rotation-v2-enums";
|
import { SecretRotation } from "./secret-rotation-v2-enums";
|
||||||
|
|
||||||
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation;
|
export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation;
|
||||||
|
|
||||||
export type TSecretRotationV2WithConnection =
|
export type TSecretRotationV2WithConnection =
|
||||||
| TPostgresCredentialsRotationWithConnection
|
| TPostgresCredentialsRotationWithConnection
|
||||||
| TMsSqlCredentialsRotationWithConnection;
|
| TMsSqlCredentialsRotationWithConnection
|
||||||
|
| TAuth0ClientSecretRotationWithConnection;
|
||||||
|
|
||||||
export type TSecretRotationV2GeneratedCredentials = TSqlCredentialsRotationGeneratedCredentials;
|
export type TSecretRotationV2GeneratedCredentials =
|
||||||
|
| TSqlCredentialsRotationGeneratedCredentials
|
||||||
|
| TAuth0ClientSecretRotationGeneratedCredentials;
|
||||||
|
|
||||||
export type TSecretRotationV2Input = TPostgresCredentialsRotationInput | TMsSqlCredentialsRotationInput;
|
export type TSecretRotationV2Input =
|
||||||
|
| TPostgresCredentialsRotationInput
|
||||||
|
| TMsSqlCredentialsRotationInput
|
||||||
|
| TAuth0ClientSecretRotationInput;
|
||||||
|
|
||||||
export type TSecretRotationV2ListItem = TPostgresCredentialsRotationListItem | TMsSqlCredentialsRotationListItem;
|
export type TSecretRotationV2ListItem =
|
||||||
|
| TPostgresCredentialsRotationListItem
|
||||||
|
| TMsSqlCredentialsRotationListItem
|
||||||
|
| TAuth0ClientSecretRotationListItem;
|
||||||
|
|
||||||
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
|
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
|
||||||
|
|
||||||
@@ -129,27 +147,34 @@ export type TSecretRotationSendNotificationJobPayload = {
|
|||||||
// transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the
|
// transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the
|
||||||
// third party credential changes (when supported), preventing credentials getting out of sync
|
// third party credential changes (when supported), preventing credentials getting out of sync
|
||||||
|
|
||||||
export type TRotationFactoryIssueCredentials = (
|
export type TRotationFactoryIssueCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||||
callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise<TSecretRotationV2Raw>
|
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>
|
||||||
) => Promise<TSecretRotationV2Raw>;
|
) => Promise<TSecretRotationV2Raw>;
|
||||||
|
|
||||||
export type TRotationFactoryRevokeCredentials = (
|
export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||||
generatedCredentials: TSecretRotationV2GeneratedCredentials,
|
generatedCredentials: T,
|
||||||
callback: () => Promise<TSecretRotationV2Raw>
|
callback: () => Promise<TSecretRotationV2Raw>
|
||||||
) => Promise<TSecretRotationV2Raw>;
|
) => Promise<TSecretRotationV2Raw>;
|
||||||
|
|
||||||
export type TRotationFactoryRotateCredentials = (
|
export type TRotationFactoryRotateCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||||
credentialsToRevoke: TSecretRotationV2GeneratedCredentials[number] | undefined,
|
credentialsToRevoke: T[number] | undefined,
|
||||||
callback: (newCredentials: TSecretRotationV2GeneratedCredentials[number]) => Promise<TSecretRotationV2Raw>
|
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>
|
||||||
) => Promise<TSecretRotationV2Raw>;
|
) => Promise<TSecretRotationV2Raw>;
|
||||||
|
|
||||||
export type TRotationFactoryGetSecretsPayload = (
|
export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||||
generatedCredentials: TSecretRotationV2GeneratedCredentials[number]
|
generatedCredentials: T[number]
|
||||||
) => { key: string; value: string }[];
|
) => { key: string; value: string }[];
|
||||||
|
|
||||||
export type TRotationFactory = (secretRotation: TSecretRotationV2WithConnection) => {
|
export type TRotationFactory<
|
||||||
issueCredentials: TRotationFactoryIssueCredentials;
|
T extends TSecretRotationV2WithConnection,
|
||||||
revokeCredentials: TRotationFactoryRevokeCredentials;
|
C extends TSecretRotationV2GeneratedCredentials
|
||||||
rotateCredentials: TRotationFactoryRotateCredentials;
|
> = (
|
||||||
getSecretsPayload: TRotationFactoryGetSecretsPayload;
|
secretRotation: T,
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
) => {
|
||||||
|
issueCredentials: TRotationFactoryIssueCredentials<C>;
|
||||||
|
revokeCredentials: TRotationFactoryRevokeCredentials<C>;
|
||||||
|
rotateCredentials: TRotationFactoryRotateCredentials<C>;
|
||||||
|
getSecretsPayload: TRotationFactoryGetSecretsPayload<C>;
|
||||||
};
|
};
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||||
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||||
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||||
|
|
||||||
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||||
PostgresCredentialsRotationSchema,
|
PostgresCredentialsRotationSchema,
|
||||||
MsSqlCredentialsRotationSchema
|
MsSqlCredentialsRotationSchema,
|
||||||
|
Auth0ClientSecretRotationSchema
|
||||||
]);
|
]);
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { randomInt } from "crypto";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
TRotationFactory,
|
||||||
TRotationFactoryGetSecretsPayload,
|
TRotationFactoryGetSecretsPayload,
|
||||||
TRotationFactoryIssueCredentials,
|
TRotationFactoryIssueCredentials,
|
||||||
TRotationFactoryRevokeCredentials,
|
TRotationFactoryRevokeCredentials,
|
||||||
@@ -8,94 +7,12 @@ import {
|
|||||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql";
|
import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql";
|
||||||
|
|
||||||
|
import { generatePassword } from "../utils";
|
||||||
import {
|
import {
|
||||||
TSqlCredentialsRotationGeneratedCredentials,
|
TSqlCredentialsRotationGeneratedCredentials,
|
||||||
TSqlCredentialsRotationWithConnection
|
TSqlCredentialsRotationWithConnection
|
||||||
} from "./sql-credentials-rotation-types";
|
} from "./sql-credentials-rotation-types";
|
||||||
|
|
||||||
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
|
||||||
length: 48,
|
|
||||||
required: {
|
|
||||||
lowercase: 1,
|
|
||||||
uppercase: 1,
|
|
||||||
digits: 1,
|
|
||||||
symbols: 0
|
|
||||||
},
|
|
||||||
allowedSymbols: "-_.~!*"
|
|
||||||
};
|
|
||||||
|
|
||||||
const generatePassword = () => {
|
|
||||||
try {
|
|
||||||
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
|
|
||||||
|
|
||||||
const chars = {
|
|
||||||
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
|
||||||
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
||||||
digits: "0123456789",
|
|
||||||
symbols: allowedSymbols || "-_.~!*"
|
|
||||||
};
|
|
||||||
|
|
||||||
const parts: string[] = [];
|
|
||||||
|
|
||||||
if (required.lowercase > 0) {
|
|
||||||
parts.push(
|
|
||||||
...Array(required.lowercase)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (required.uppercase > 0) {
|
|
||||||
parts.push(
|
|
||||||
...Array(required.uppercase)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (required.digits > 0) {
|
|
||||||
parts.push(
|
|
||||||
...Array(required.digits)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => chars.digits[randomInt(chars.digits.length)])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (required.symbols > 0) {
|
|
||||||
parts.push(
|
|
||||||
...Array(required.symbols)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => chars.symbols[randomInt(chars.symbols.length)])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
|
|
||||||
const remainingLength = Math.max(length - requiredTotal, 0);
|
|
||||||
|
|
||||||
const allowedChars = Object.entries(chars)
|
|
||||||
.filter(([key]) => required[key as keyof typeof required] > 0)
|
|
||||||
.map(([, value]) => value)
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
parts.push(
|
|
||||||
...Array(remainingLength)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => allowedChars[randomInt(allowedChars.length)])
|
|
||||||
);
|
|
||||||
|
|
||||||
// shuffle the array to mix up the characters
|
|
||||||
for (let i = parts.length - 1; i > 0; i -= 1) {
|
|
||||||
const j = randomInt(i + 1);
|
|
||||||
[parts[i], parts[j]] = [parts[j], parts[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join("");
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
|
||||||
throw new Error(`Failed to generate password: ${message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGeneratedCredentials) => {
|
const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGeneratedCredentials) => {
|
||||||
const error = e as Error;
|
const error = e as Error;
|
||||||
|
|
||||||
@@ -110,7 +27,10 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat
|
|||||||
return redactedMessage;
|
return redactedMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRotationWithConnection) => {
|
export const sqlCredentialsRotationFactory: TRotationFactory<
|
||||||
|
TSqlCredentialsRotationWithConnection,
|
||||||
|
TSqlCredentialsRotationGeneratedCredentials
|
||||||
|
> = (secretRotation) => {
|
||||||
const {
|
const {
|
||||||
connection,
|
connection,
|
||||||
parameters: { username1, username2 },
|
parameters: { username1, username2 },
|
||||||
@@ -118,7 +38,7 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
|
|||||||
secretsMapping
|
secretsMapping
|
||||||
} = secretRotation;
|
} = secretRotation;
|
||||||
|
|
||||||
const validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
|
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
|
||||||
const client = await getSqlConnectionClient({
|
const client = await getSqlConnectionClient({
|
||||||
...connection,
|
...connection,
|
||||||
credentials: {
|
credentials: {
|
||||||
@@ -136,7 +56,9 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const issueCredentials: TRotationFactoryIssueCredentials = async (callback) => {
|
const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
const client = await getSqlConnectionClient(connection);
|
const client = await getSqlConnectionClient(connection);
|
||||||
|
|
||||||
// For SQL, since we get existing users, we change both their passwords
|
// For SQL, since we get existing users, we change both their passwords
|
||||||
@@ -159,13 +81,16 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
|
|||||||
}
|
}
|
||||||
|
|
||||||
for await (const credentials of credentialsSet) {
|
for await (const credentials of credentialsSet) {
|
||||||
await validateCredentials(credentials);
|
await $validateCredentials(credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(credentialsSet[0]);
|
return callback(credentialsSet[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const revokeCredentials: TRotationFactoryRevokeCredentials = async (credentialsToRevoke, callback) => {
|
const revokeCredentials: TRotationFactoryRevokeCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
|
||||||
|
credentialsToRevoke,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
const client = await getSqlConnectionClient(connection);
|
const client = await getSqlConnectionClient(connection);
|
||||||
|
|
||||||
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
|
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
|
||||||
@@ -186,7 +111,10 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
|
|||||||
return callback();
|
return callback();
|
||||||
};
|
};
|
||||||
|
|
||||||
const rotateCredentials: TRotationFactoryRotateCredentials = async (_, callback) => {
|
const rotateCredentials: TRotationFactoryRotateCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
|
||||||
|
_,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
const client = await getSqlConnectionClient(connection);
|
const client = await getSqlConnectionClient(connection);
|
||||||
|
|
||||||
// generate new password for the next active user
|
// generate new password for the next active user
|
||||||
@@ -200,12 +128,14 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
|
|||||||
await client.destroy();
|
await client.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
await validateCredentials(credentials);
|
await $validateCredentials(credentials);
|
||||||
|
|
||||||
return callback(credentials);
|
return callback(credentials);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSecretsPayload: TRotationFactoryGetSecretsPayload = (generatedCredentials) => {
|
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TSqlCredentialsRotationGeneratedCredentials> = (
|
||||||
|
generatedCredentials
|
||||||
|
) => {
|
||||||
const { username, password } = secretsMapping;
|
const { username, password } = secretsMapping;
|
||||||
|
|
||||||
const secrets = [
|
const secrets = [
|
||||||
@@ -226,7 +156,6 @@ export const sqlCredentialsRotationFactory = (secretRotation: TSqlCredentialsRot
|
|||||||
issueCredentials,
|
issueCredentials,
|
||||||
revokeCredentials,
|
revokeCredentials,
|
||||||
rotateCredentials,
|
rotateCredentials,
|
||||||
getSecretsPayload,
|
getSecretsPayload
|
||||||
validateCredentials
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -0,0 +1,84 @@
|
|||||||
|
import { randomInt } from "crypto";
|
||||||
|
|
||||||
|
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||||
|
length: 48,
|
||||||
|
required: {
|
||||||
|
lowercase: 1,
|
||||||
|
uppercase: 1,
|
||||||
|
digits: 1,
|
||||||
|
symbols: 0
|
||||||
|
},
|
||||||
|
allowedSymbols: "-_.~!*"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generatePassword = () => {
|
||||||
|
try {
|
||||||
|
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
|
||||||
|
|
||||||
|
const chars = {
|
||||||
|
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
||||||
|
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
|
digits: "0123456789",
|
||||||
|
symbols: allowedSymbols || "-_.~!*"
|
||||||
|
};
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (required.lowercase > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.lowercase)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.lowercase[randomInt(chars.lowercase.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.uppercase > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.uppercase)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.uppercase[randomInt(chars.uppercase.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.digits > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.digits)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.digits[randomInt(chars.digits.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (required.symbols > 0) {
|
||||||
|
parts.push(
|
||||||
|
...Array(required.symbols)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => chars.symbols[randomInt(chars.symbols.length)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
|
||||||
|
const remainingLength = Math.max(length - requiredTotal, 0);
|
||||||
|
|
||||||
|
const allowedChars = Object.entries(chars)
|
||||||
|
.filter(([key]) => required[key as keyof typeof required] > 0)
|
||||||
|
.map(([, value]) => value)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
parts.push(
|
||||||
|
...Array(remainingLength)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => allowedChars[randomInt(allowedChars.length)])
|
||||||
|
);
|
||||||
|
|
||||||
|
// shuffle the array to mix up the characters
|
||||||
|
for (let i = parts.length - 1; i > 0; i -= 1) {
|
||||||
|
const j = randomInt(i + 1);
|
||||||
|
[parts[i], parts[j]] = [parts[j], parts[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join("");
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
throw new Error(`Failed to generate password: ${message}`);
|
||||||
|
}
|
||||||
|
};
|
@@ -127,6 +127,13 @@ export const secretRotationServiceFactory = ({
|
|||||||
});
|
});
|
||||||
if (selectedSecrets.length !== Object.values(outputs).length)
|
if (selectedSecrets.length !== Object.values(outputs).length)
|
||||||
throw new NotFoundError({ message: `Secrets not found in folder with ID '${folder.id}'` });
|
throw new NotFoundError({ message: `Secrets not found in folder with ID '${folder.id}'` });
|
||||||
|
const rotatedSecrets = selectedSecrets.filter(({ isRotatedSecret }) => isRotatedSecret);
|
||||||
|
if (rotatedSecrets.length)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Selected secrets are already used for rotation: ${rotatedSecrets
|
||||||
|
.map((secret) => secret.key)
|
||||||
|
.join(", ")}`
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const selectedSecrets = await secretDAL.find({
|
const selectedSecrets = await secretDAL.find({
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
|
@@ -18,7 +18,8 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
|
|||||||
title: "PostgreSQL",
|
title: "PostgreSQL",
|
||||||
image: "postgres.png",
|
image: "postgres.png",
|
||||||
description: "Rotate PostgreSQL/CockroachDB user credentials",
|
description: "Rotate PostgreSQL/CockroachDB user credentials",
|
||||||
template: POSTGRES_TEMPLATE
|
template: POSTGRES_TEMPLATE,
|
||||||
|
isDeprecated: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "mysql",
|
name: "mysql",
|
||||||
@@ -32,7 +33,8 @@ export const rotationTemplates: TSecretRotationProviderTemplate[] = [
|
|||||||
title: "Microsoft SQL Server",
|
title: "Microsoft SQL Server",
|
||||||
image: "mssqlserver.png",
|
image: "mssqlserver.png",
|
||||||
description: "Rotate Microsoft SQL server user credentials",
|
description: "Rotate Microsoft SQL server user credentials",
|
||||||
template: MSSQL_TEMPLATE
|
template: MSSQL_TEMPLATE,
|
||||||
|
isDeprecated: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "aws-iam",
|
name: "aws-iam",
|
||||||
|
@@ -50,6 +50,7 @@ export type TSecretRotationProviderTemplate = {
|
|||||||
image?: string;
|
image?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
template: THttpProviderTemplate | TDbProviderTemplate | TAwsProviderTemplate;
|
template: THttpProviderTemplate | TDbProviderTemplate | TAwsProviderTemplate;
|
||||||
|
isDeprecated?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type THttpProviderTemplate = {
|
export type THttpProviderTemplate = {
|
||||||
|
@@ -478,7 +478,8 @@ export const PROJECTS = {
|
|||||||
name: "The new name of the project.",
|
name: "The new name of the project.",
|
||||||
projectDescription: "An optional description label for the project.",
|
projectDescription: "An optional description label for the project.",
|
||||||
autoCapitalization: "Disable or enable auto-capitalization for the project.",
|
autoCapitalization: "Disable or enable auto-capitalization for the project.",
|
||||||
slug: "An optional slug for the project. (must be unique within the organization)"
|
slug: "An optional slug for the project. (must be unique within the organization)",
|
||||||
|
hasDeleteProtection: "Enable or disable delete protection for the project."
|
||||||
},
|
},
|
||||||
GET_KEY: {
|
GET_KEY: {
|
||||||
workspaceId: "The ID of the project to get the key from."
|
workspaceId: "The ID of the project to get the key from."
|
||||||
@@ -1782,6 +1783,12 @@ export const AppConnections = {
|
|||||||
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to be deleted.`
|
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to be deleted.`
|
||||||
}),
|
}),
|
||||||
CREDENTIALS: {
|
CREDENTIALS: {
|
||||||
|
AUTH0_CONNECTION: {
|
||||||
|
domain: "The domain of the Auth0 instance to connect to.",
|
||||||
|
clientId: "Your Auth0 application's Client ID.",
|
||||||
|
clientSecret: "Your Auth0 application's Client Secret.",
|
||||||
|
audience: "The unique identifier of the target API you want to access."
|
||||||
|
},
|
||||||
SQL_CONNECTION: {
|
SQL_CONNECTION: {
|
||||||
host: "The hostname of the database server.",
|
host: "The hostname of the database server.",
|
||||||
port: "The port number of the database.",
|
port: "The port number of the database.",
|
||||||
@@ -1801,6 +1808,10 @@ export const AppConnections = {
|
|||||||
CAMUNDA: {
|
CAMUNDA: {
|
||||||
clientId: "The client ID used to authenticate with Camunda.",
|
clientId: "The client ID used to authenticate with Camunda.",
|
||||||
clientSecret: "The client secret used to authenticate with Camunda."
|
clientSecret: "The client secret used to authenticate with Camunda."
|
||||||
|
},
|
||||||
|
WINDMILL: {
|
||||||
|
instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).",
|
||||||
|
accessToken: "The access token to use to connect with Windmill."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1936,6 +1947,10 @@ export const SecretSyncs = {
|
|||||||
env: "The ID of the Vercel environment to sync secrets to.",
|
env: "The ID of the Vercel environment to sync secrets to.",
|
||||||
branch: "The branch to sync preview secrets to.",
|
branch: "The branch to sync preview secrets to.",
|
||||||
teamId: "The ID of the Vercel team to sync secrets to."
|
teamId: "The ID of the Vercel team to sync secrets to."
|
||||||
|
},
|
||||||
|
WINDMILL: {
|
||||||
|
workspace: "The Windmill workspace to sync secrets to.",
|
||||||
|
path: "The Windmill workspace path to sync secrets to."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1997,12 +2012,19 @@ export const SecretRotations = {
|
|||||||
"The username of the first login to rotate passwords for. This user must already exists in your database.",
|
"The username of the first login to rotate passwords for. This user must already exists in your database.",
|
||||||
username2:
|
username2:
|
||||||
"The username of the second login to rotate passwords for. This user must already exists in your database."
|
"The username of the second login to rotate passwords for. This user must already exists in your database."
|
||||||
|
},
|
||||||
|
AUTH0_CLIENT_SECRET: {
|
||||||
|
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SECRETS_MAPPING: {
|
SECRETS_MAPPING: {
|
||||||
SQL_CREDENTIALS: {
|
SQL_CREDENTIALS: {
|
||||||
username: "The name of the secret that the active username will be mapped to.",
|
username: "The name of the secret that the active username will be mapped to.",
|
||||||
password: "The name of the secret that the generated password will be mapped to."
|
password: "The name of the secret that the generated password will be mapped to."
|
||||||
|
},
|
||||||
|
AUTH0_CLIENT_SECRET: {
|
||||||
|
clientId: "The name of the secret that the client ID will be mapped to.",
|
||||||
|
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -24,5 +24,6 @@ export enum PermissionConditionOperators {
|
|||||||
$IN = "$in",
|
$IN = "$in",
|
||||||
$EQ = "$eq",
|
$EQ = "$eq",
|
||||||
$NEQ = "$ne",
|
$NEQ = "$ne",
|
||||||
$GLOB = "$glob"
|
$GLOB = "$glob",
|
||||||
|
$ELEMENTMATCH = "$elemMatch"
|
||||||
}
|
}
|
||||||
|
@@ -197,6 +197,7 @@ const envSchema = z
|
|||||||
/* ----------------------------------------------------------------------------- */
|
/* ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/* App Connections ----------------------------------------------------------------------------- */
|
/* App Connections ----------------------------------------------------------------------------- */
|
||||||
|
ALLOW_INTERNAL_IP_CONNECTIONS: zodStrBool.default("false"),
|
||||||
|
|
||||||
// aws
|
// aws
|
||||||
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()),
|
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()),
|
||||||
|
@@ -118,7 +118,12 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const $signRsaDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => {
|
const $signRsaDigest = async (
|
||||||
|
digest: Buffer,
|
||||||
|
privateKey: Buffer,
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm,
|
||||||
|
signingAlgorithm: SigningAlgorithm
|
||||||
|
) => {
|
||||||
const tempDir = await createTemporaryDirectory("kms-rsa-sign");
|
const tempDir = await createTemporaryDirectory("kms-rsa-sign");
|
||||||
const digestPath = path.join(tempDir, "digest.bin");
|
const digestPath = path.join(tempDir, "digest.bin");
|
||||||
const sigPath = path.join(tempDir, "signature.bin");
|
const sigPath = path.join(tempDir, "signature.bin");
|
||||||
@@ -164,12 +169,22 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
|
|||||||
}
|
}
|
||||||
|
|
||||||
return signature;
|
return signature;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "KMS: Failed to sign RSA digest");
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to sign RSA digest with ${signingAlgorithm} due to signing error. Ensure that your digest is hashed with ${hashAlgorithm.toUpperCase()}.`
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
await cleanTemporaryDirectory(tempDir);
|
await cleanTemporaryDirectory(tempDir);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const $signEccDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => {
|
const $signEccDigest = async (
|
||||||
|
digest: Buffer,
|
||||||
|
privateKey: Buffer,
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm,
|
||||||
|
signingAlgorithm: SigningAlgorithm
|
||||||
|
) => {
|
||||||
const tempDir = await createTemporaryDirectory("ecc-sign");
|
const tempDir = await createTemporaryDirectory("ecc-sign");
|
||||||
const digestPath = path.join(tempDir, "digest.bin");
|
const digestPath = path.join(tempDir, "digest.bin");
|
||||||
const keyPath = path.join(tempDir, "key.pem");
|
const keyPath = path.join(tempDir, "key.pem");
|
||||||
@@ -216,6 +231,11 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
|
|||||||
}
|
}
|
||||||
|
|
||||||
return signature;
|
return signature;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, "KMS: Failed to sign ECC digest");
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to sign ECC digest with ${signingAlgorithm} due to signing error. Ensure that your digest is hashed with ${hashAlgorithm.toUpperCase()}.`
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
await cleanTemporaryDirectory(tempDir);
|
await cleanTemporaryDirectory(tempDir);
|
||||||
}
|
}
|
||||||
@@ -329,7 +349,12 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
|
|||||||
|
|
||||||
const signDigestFunctionsMap: Record<
|
const signDigestFunctionsMap: Record<
|
||||||
AsymmetricKeyAlgorithm,
|
AsymmetricKeyAlgorithm,
|
||||||
(data: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise<Buffer>
|
(
|
||||||
|
data: Buffer,
|
||||||
|
privateKey: Buffer,
|
||||||
|
hashAlgorithm: SupportedHashAlgorithm,
|
||||||
|
signingAlgorithm: SigningAlgorithm
|
||||||
|
) => Promise<Buffer>
|
||||||
> = {
|
> = {
|
||||||
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $signEccDigest,
|
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $signEccDigest,
|
||||||
[AsymmetricKeyAlgorithm.RSA_4096]: $signRsaDigest
|
[AsymmetricKeyAlgorithm.RSA_4096]: $signRsaDigest
|
||||||
@@ -360,7 +385,7 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const signature = await signFunction(data, privateKey, hashAlgorithm);
|
const signature = await signFunction(data, privateKey, hashAlgorithm, signingAlgorithm);
|
||||||
return signature;
|
return signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,10 +2,16 @@ import dns from "node:dns/promises";
|
|||||||
|
|
||||||
import { isIPv4 } from "net";
|
import { isIPv4 } from "net";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
import { BadRequestError } from "../errors";
|
import { BadRequestError } from "../errors";
|
||||||
import { isPrivateIp } from "../ip/ipRange";
|
import { isPrivateIp } from "../ip/ipRange";
|
||||||
|
|
||||||
export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
if (appCfg.isDevelopmentMode) return;
|
||||||
|
|
||||||
const validUrl = new URL(url);
|
const validUrl = new URL(url);
|
||||||
const inputHostIps: string[] = [];
|
const inputHostIps: string[] = [];
|
||||||
if (isIPv4(validUrl.host)) {
|
if (isIPv4(validUrl.host)) {
|
||||||
@@ -18,7 +24,8 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
|||||||
inputHostIps.push(...resolvedIps);
|
inputHostIps.push(...resolvedIps);
|
||||||
}
|
}
|
||||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||||
if (isInternalIp) throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
if (isInternalIp && !appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)
|
||||||
|
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||||
};
|
};
|
||||||
|
|
||||||
type FQDNOptions = {
|
type FQDNOptions = {
|
||||||
|
@@ -1255,7 +1255,8 @@ export const registerRoutes = async (
|
|||||||
userDAL,
|
userDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
accessTokenQueue
|
accessTokenQueue,
|
||||||
|
smtpService
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityService = identityServiceFactory({
|
const identityService = identityServiceFactory({
|
||||||
@@ -1391,7 +1392,8 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectGatewayDAL
|
projectGatewayDAL,
|
||||||
|
resourceMetadataDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||||
@@ -1415,7 +1417,8 @@ export const registerRoutes = async (
|
|||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
secretVersionV2DAL: secretVersionV2BridgeDAL,
|
secretVersionV2DAL: secretVersionV2BridgeDAL,
|
||||||
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL
|
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL,
|
||||||
|
serviceTokenService
|
||||||
});
|
});
|
||||||
|
|
||||||
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
|
const dailyExpiringPkiItemAlert = dailyExpiringPkiItemAlertQueueServiceFactory({
|
||||||
@@ -1548,7 +1551,8 @@ export const registerRoutes = async (
|
|||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
snapshotService,
|
snapshotService,
|
||||||
secretQueueService,
|
secretQueueService,
|
||||||
queueService
|
queueService,
|
||||||
|
appConnectionDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
await secretRotationV2QueueServiceFactory({
|
await secretRotationV2QueueServiceFactory({
|
||||||
|
@@ -11,6 +11,7 @@ import {
|
|||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
|
|
||||||
import { UnpackedPermissionSchema } from "./sanitizedSchema/permission";
|
import { UnpackedPermissionSchema } from "./sanitizedSchema/permission";
|
||||||
|
|
||||||
@@ -232,7 +233,11 @@ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
|||||||
inputIV: true,
|
inputIV: true,
|
||||||
inputTag: true,
|
inputTag: true,
|
||||||
algorithm: true
|
algorithm: true
|
||||||
});
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
metadata: ResourceMetadataSchema.optional()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const SanitizedAuditLogStreamSchema = z.object({
|
export const SanitizedAuditLogStreamSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -255,7 +260,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
|
|||||||
upgradeStatus: true,
|
upgradeStatus: true,
|
||||||
pitVersionLimit: true,
|
pitVersionLimit: true,
|
||||||
kmsCertificateKeyId: true,
|
kmsCertificateKeyId: true,
|
||||||
auditLogsRetentionDays: true
|
auditLogsRetentionDays: true,
|
||||||
|
hasDeleteProtection: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SanitizedTagSchema = SecretTagsSchema.pick({
|
export const SanitizedTagSchema = SecretTagsSchema.pick({
|
||||||
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { Auth0ConnectionListItemSchema, SanitizedAuth0ConnectionSchema } from "@app/services/app-connection/auth0";
|
||||||
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
|
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
|
||||||
import {
|
import {
|
||||||
AzureAppConfigurationConnectionListItemSchema,
|
AzureAppConfigurationConnectionListItemSchema,
|
||||||
@@ -36,6 +37,10 @@ import {
|
|||||||
TerraformCloudConnectionListItemSchema
|
TerraformCloudConnectionListItemSchema
|
||||||
} from "@app/services/app-connection/terraform-cloud";
|
} from "@app/services/app-connection/terraform-cloud";
|
||||||
import { SanitizedVercelConnectionSchema, VercelConnectionListItemSchema } from "@app/services/app-connection/vercel";
|
import { SanitizedVercelConnectionSchema, VercelConnectionListItemSchema } from "@app/services/app-connection/vercel";
|
||||||
|
import {
|
||||||
|
SanitizedWindmillConnectionSchema,
|
||||||
|
WindmillConnectionListItemSchema
|
||||||
|
} from "@app/services/app-connection/windmill";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
// can't use discriminated due to multiple schemas for certain apps
|
// can't use discriminated due to multiple schemas for certain apps
|
||||||
@@ -51,7 +56,9 @@ const SanitizedAppConnectionSchema = z.union([
|
|||||||
...SanitizedVercelConnectionSchema.options,
|
...SanitizedVercelConnectionSchema.options,
|
||||||
...SanitizedPostgresConnectionSchema.options,
|
...SanitizedPostgresConnectionSchema.options,
|
||||||
...SanitizedMsSqlConnectionSchema.options,
|
...SanitizedMsSqlConnectionSchema.options,
|
||||||
...SanitizedCamundaConnectionSchema.options
|
...SanitizedCamundaConnectionSchema.options,
|
||||||
|
...SanitizedWindmillConnectionSchema.options,
|
||||||
|
...SanitizedAuth0ConnectionSchema.options
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||||
@@ -66,7 +73,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
|||||||
VercelConnectionListItemSchema,
|
VercelConnectionListItemSchema,
|
||||||
PostgresConnectionListItemSchema,
|
PostgresConnectionListItemSchema,
|
||||||
MsSqlConnectionListItemSchema,
|
MsSqlConnectionListItemSchema,
|
||||||
CamundaConnectionListItemSchema
|
CamundaConnectionListItemSchema,
|
||||||
|
WindmillConnectionListItemSchema,
|
||||||
|
Auth0ConnectionListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
@@ -0,0 +1,51 @@
|
|||||||
|
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 {
|
||||||
|
CreateAuth0ConnectionSchema,
|
||||||
|
SanitizedAuth0ConnectionSchema,
|
||||||
|
UpdateAuth0ConnectionSchema
|
||||||
|
} from "@app/services/app-connection/auth0";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerAuth0ConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.Auth0,
|
||||||
|
server,
|
||||||
|
sanitizedResponseSchema: SanitizedAuth0ConnectionSchema,
|
||||||
|
createSchema: CreateAuth0ConnectionSchema,
|
||||||
|
updateSchema: UpdateAuth0ConnectionSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
// The below endpoints are not exposed and for Infisical App use
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/clients`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
clients: z.object({ name: z.string(), id: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const clients = await server.services.appConnection.auth0.listClients(connectionId, req.permission);
|
||||||
|
|
||||||
|
return { clients };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||||
@@ -12,6 +13,7 @@ import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
|||||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||||
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
|
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
|
||||||
import { registerVercelConnectionRouter } from "./vercel-connection-router";
|
import { registerVercelConnectionRouter } from "./vercel-connection-router";
|
||||||
|
import { registerWindmillConnectionRouter } from "./windmill-connection-router";
|
||||||
|
|
||||||
export * from "./app-connection-router";
|
export * from "./app-connection-router";
|
||||||
|
|
||||||
@@ -28,5 +30,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
|||||||
[AppConnection.Vercel]: registerVercelConnectionRouter,
|
[AppConnection.Vercel]: registerVercelConnectionRouter,
|
||||||
[AppConnection.Postgres]: registerPostgresConnectionRouter,
|
[AppConnection.Postgres]: registerPostgresConnectionRouter,
|
||||||
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
|
[AppConnection.MsSql]: registerMsSqlConnectionRouter,
|
||||||
[AppConnection.Camunda]: registerCamundaConnectionRouter
|
[AppConnection.Camunda]: registerCamundaConnectionRouter,
|
||||||
|
[AppConnection.Windmill]: registerWindmillConnectionRouter,
|
||||||
|
[AppConnection.Auth0]: registerAuth0ConnectionRouter
|
||||||
};
|
};
|
||||||
|
@@ -0,0 +1,53 @@
|
|||||||
|
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 {
|
||||||
|
CreateWindmillConnectionSchema,
|
||||||
|
SanitizedWindmillConnectionSchema,
|
||||||
|
UpdateWindmillConnectionSchema
|
||||||
|
} from "@app/services/app-connection/windmill";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerWindmillConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.Windmill,
|
||||||
|
server,
|
||||||
|
sanitizedResponseSchema: SanitizedWindmillConnectionSchema,
|
||||||
|
createSchema: CreateWindmillConnectionSchema,
|
||||||
|
updateSchema: UpdateWindmillConnectionSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
// The below endpoints are not exposed and for Infisical App use
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/workspaces`,
|
||||||
|
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 workspaces = await server.services.appConnection.windmill.listWorkspaces(connectionId, req.permission);
|
||||||
|
|
||||||
|
return workspaces;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,13 +1,9 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas";
|
import { SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas";
|
||||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import {
|
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||||
ProjectPermissionDynamicSecretActions,
|
|
||||||
ProjectPermissionSecretActions,
|
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/ee/services/permission/project-permission";
|
|
||||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||||
import { DASHBOARD } from "@app/lib/api-docs";
|
import { DASHBOARD } from "@app/lib/api-docs";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
@@ -142,6 +138,34 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
importedByEnvs: z
|
||||||
|
.object({
|
||||||
|
environment: z.string(),
|
||||||
|
importedBy: z
|
||||||
|
.object({
|
||||||
|
environment: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
}),
|
||||||
|
folders: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
isImported: z.boolean(),
|
||||||
|
secrets: z
|
||||||
|
.object({
|
||||||
|
secretId: z.string(),
|
||||||
|
referencedSecretKey: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
totalFolderCount: z.number().optional(),
|
totalFolderCount: z.number().optional(),
|
||||||
totalDynamicSecretCount: z.number().optional(),
|
totalDynamicSecretCount: z.number().optional(),
|
||||||
totalSecretCount: z.number().optional(),
|
totalSecretCount: z.number().optional(),
|
||||||
@@ -289,24 +313,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalCount: totalFolderCount ?? 0
|
totalCount: totalFolderCount ?? 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const { permission } = await server.services.permission.getProjectPermission({
|
if (includeDynamicSecrets) {
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
projectId,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
|
||||||
});
|
|
||||||
|
|
||||||
const allowedDynamicSecretEnvironments = // filter envs user has access to
|
|
||||||
environments.filter((environment) =>
|
|
||||||
permission.can(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (includeDynamicSecrets && allowedDynamicSecretEnvironments.length) {
|
|
||||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||||
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -315,7 +322,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId,
|
projectId,
|
||||||
search,
|
search,
|
||||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
environmentSlugs: environments,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
isInternal: true
|
isInternal: true
|
||||||
});
|
});
|
||||||
@@ -330,7 +337,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
search,
|
search,
|
||||||
orderBy,
|
orderBy,
|
||||||
orderDirection,
|
orderDirection,
|
||||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
environmentSlugs: environments,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
limit: remainingLimit,
|
limit: remainingLimit,
|
||||||
offset: adjustedOffset,
|
offset: adjustedOffset,
|
||||||
@@ -471,6 +478,28 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const importedByEnvs = [];
|
||||||
|
|
||||||
|
for await (const environment of environments) {
|
||||||
|
const importedBy = await server.services.secretImport.getFolderIsImportedBy({
|
||||||
|
path: secretPath,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
secrets: secrets?.filter((s) => s.environment === environment)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (importedBy) {
|
||||||
|
importedByEnvs.push({
|
||||||
|
environment,
|
||||||
|
importedBy
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
folders,
|
folders,
|
||||||
dynamicSecrets,
|
dynamicSecrets,
|
||||||
@@ -482,6 +511,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalImportCount,
|
totalImportCount,
|
||||||
totalSecretCount,
|
totalSecretCount,
|
||||||
totalSecretRotationCount,
|
totalSecretRotationCount,
|
||||||
|
importedByEnvs,
|
||||||
totalCount:
|
totalCount:
|
||||||
(totalFolderCount ?? 0) +
|
(totalFolderCount ?? 0) +
|
||||||
(totalDynamicSecretCount ?? 0) +
|
(totalDynamicSecretCount ?? 0) +
|
||||||
@@ -575,6 +605,28 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalFolderCount: z.number().optional(),
|
totalFolderCount: z.number().optional(),
|
||||||
totalDynamicSecretCount: z.number().optional(),
|
totalDynamicSecretCount: z.number().optional(),
|
||||||
totalSecretCount: z.number().optional(),
|
totalSecretCount: z.number().optional(),
|
||||||
|
importedBy: z
|
||||||
|
.object({
|
||||||
|
environment: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
}),
|
||||||
|
folders: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
isImported: z.boolean(),
|
||||||
|
secrets: z
|
||||||
|
.object({
|
||||||
|
secretId: z.string(),
|
||||||
|
referencedSecretKey: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
totalSecretRotationCount: z.number().optional(),
|
totalSecretRotationCount: z.number().optional(),
|
||||||
totalCount: z.number()
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
@@ -835,6 +887,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const importedBy = await server.services.secretImport.getFolderIsImportedBy({
|
||||||
|
path: secretPath,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
secrets
|
||||||
|
});
|
||||||
|
|
||||||
if (secrets?.length || secretRotations?.length) {
|
if (secrets?.length || secretRotations?.length) {
|
||||||
const secretCount =
|
const secretCount =
|
||||||
(secrets?.length ?? 0) +
|
(secrets?.length ?? 0) +
|
||||||
@@ -880,6 +943,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalDynamicSecretCount,
|
totalDynamicSecretCount,
|
||||||
totalSecretCount,
|
totalSecretCount,
|
||||||
totalSecretRotationCount,
|
totalSecretRotationCount,
|
||||||
|
importedBy,
|
||||||
totalCount:
|
totalCount:
|
||||||
(totalImportCount ?? 0) +
|
(totalImportCount ?? 0) +
|
||||||
(totalFolderCount ?? 0) +
|
(totalFolderCount ?? 0) +
|
||||||
|
@@ -31,6 +31,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
|||||||
.object({
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
|
syncSlug: z.string().optional(),
|
||||||
clientSlug: z.string().optional(),
|
clientSlug: z.string().optional(),
|
||||||
image: z.string(),
|
image: z.string(),
|
||||||
isAvailable: z.boolean().optional(),
|
isAvailable: z.boolean().optional(),
|
||||||
|
@@ -31,7 +31,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
organizations: sanitizedOrganizationSchema
|
organizations: sanitizedOrganizationSchema
|
||||||
.extend({
|
.extend({
|
||||||
orgAuthMethod: z.string()
|
orgAuthMethod: z.string(),
|
||||||
|
userRole: z.string()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
})
|
})
|
||||||
@@ -259,7 +260,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
|
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
|
||||||
enforceMfa: z.boolean().optional(),
|
enforceMfa: z.boolean().optional(),
|
||||||
selectedMfaMethod: z.nativeEnum(MfaMethod).optional(),
|
selectedMfaMethod: z.nativeEnum(MfaMethod).optional(),
|
||||||
allowSecretSharingOutsideOrganization: z.boolean().optional()
|
allowSecretSharingOutsideOrganization: z.boolean().optional(),
|
||||||
|
bypassOrgAuthEnabled: z.boolean().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@@ -312,6 +312,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECTS.UPDATE.projectDescription),
|
.describe(PROJECTS.UPDATE.projectDescription),
|
||||||
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
|
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
|
||||||
|
hasDeleteProtection: z.boolean().optional().describe(PROJECTS.UPDATE.hasDeleteProtection),
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -340,6 +341,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
description: req.body.description,
|
description: req.body.description,
|
||||||
autoCapitalization: req.body.autoCapitalization,
|
autoCapitalization: req.body.autoCapitalization,
|
||||||
|
hasDeleteProtection: req.body.hasDeleteProtection,
|
||||||
slug: req.body.slug
|
slug: req.body.slug
|
||||||
},
|
},
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
@@ -390,6 +392,43 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:workspaceId/delete-protection",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
hasDeleteProtection: z.boolean()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
workspace: SanitizedProjectSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspace = await server.services.project.toggleDeleteProtection({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
hasDeleteProtection: req.body.hasDeleteProtection
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
message: "Successfully changed workspace settings",
|
||||||
|
workspace
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
url: "/:workspaceSlug/version-limit",
|
url: "/:workspaceSlug/version-limit",
|
||||||
|
@@ -39,17 +39,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.CREATE.path),
|
.describe(FOLDERS.CREATE.path)
|
||||||
|
.optional(),
|
||||||
// backward compatiability with cli
|
// backward compatiability with cli
|
||||||
directory: z
|
directory: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.CREATE.directory),
|
.describe(FOLDERS.CREATE.directory)
|
||||||
|
.optional(),
|
||||||
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
|
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -60,7 +62,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const path = req.body.path || req.body.directory;
|
const path = req.body.path || req.body.directory || "/";
|
||||||
const folder = await server.services.folder.createFolder({
|
const folder = await server.services.folder.createFolder({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -120,17 +122,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.UPDATE.path),
|
.describe(FOLDERS.UPDATE.path)
|
||||||
|
.optional(),
|
||||||
// backward compatiability with cli
|
// backward compatiability with cli
|
||||||
directory: z
|
directory: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.UPDATE.directory),
|
.describe(FOLDERS.UPDATE.directory)
|
||||||
|
.optional(),
|
||||||
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
|
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -141,7 +145,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const path = req.body.path || req.body.directory;
|
const path = req.body.path || req.body.directory || "/";
|
||||||
const { folder, old } = await server.services.folder.updateFolder({
|
const { folder, old } = await server.services.folder.updateFolder({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -271,17 +275,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.DELETE.path),
|
.describe(FOLDERS.DELETE.path)
|
||||||
|
.optional(),
|
||||||
// keep this here as cli need directory
|
// keep this here as cli need directory
|
||||||
directory: z
|
directory: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.DELETE.directory)
|
.describe(FOLDERS.DELETE.directory)
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -291,7 +297,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const path = req.body.path || req.body.directory;
|
const path = req.body.path || req.body.directory || "/";
|
||||||
const folder = await server.services.folder.deleteFolder({
|
const folder = await server.services.folder.deleteFolder({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -339,18 +345,18 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
path: z
|
path: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||||
.transform(prefixWithSlash)
|
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.LIST.path),
|
.describe(FOLDERS.LIST.path)
|
||||||
|
.optional(),
|
||||||
// backward compatiability with cli
|
// backward compatiability with cli
|
||||||
directory: z
|
directory: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||||
.transform(prefixWithSlash)
|
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.LIST.directory),
|
.describe(FOLDERS.LIST.directory)
|
||||||
|
.optional(),
|
||||||
recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive)
|
recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -363,7 +369,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const path = req.query.path || req.query.directory;
|
const path = req.query.path || req.query.directory || "/";
|
||||||
const folders = await server.services.folder.getFolders({
|
const folders = await server.services.folder.getFolders({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
|
@@ -11,6 +11,7 @@ import { registerGitHubSyncRouter } from "./github-sync-router";
|
|||||||
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||||
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
|
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
|
||||||
import { registerVercelSyncRouter } from "./vercel-sync-router";
|
import { registerVercelSyncRouter } from "./vercel-sync-router";
|
||||||
|
import { registerWindmillSyncRouter } from "./windmill-sync-router";
|
||||||
|
|
||||||
export * from "./secret-sync-router";
|
export * from "./secret-sync-router";
|
||||||
|
|
||||||
@@ -25,5 +26,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
|||||||
[SecretSync.Humanitec]: registerHumanitecSyncRouter,
|
[SecretSync.Humanitec]: registerHumanitecSyncRouter,
|
||||||
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
|
[SecretSync.TerraformCloud]: registerTerraformCloudSyncRouter,
|
||||||
[SecretSync.Camunda]: registerCamundaSyncRouter,
|
[SecretSync.Camunda]: registerCamundaSyncRouter,
|
||||||
[SecretSync.Vercel]: registerVercelSyncRouter
|
[SecretSync.Vercel]: registerVercelSyncRouter,
|
||||||
|
[SecretSync.Windmill]: registerWindmillSyncRouter
|
||||||
};
|
};
|
||||||
|
@@ -25,6 +25,7 @@ import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret
|
|||||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||||
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
|
||||||
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
|
||||||
|
import { WindmillSyncListItemSchema, WindmillSyncSchema } from "@app/services/secret-sync/windmill";
|
||||||
|
|
||||||
const SecretSyncSchema = z.discriminatedUnion("destination", [
|
const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||||
AwsParameterStoreSyncSchema,
|
AwsParameterStoreSyncSchema,
|
||||||
@@ -37,7 +38,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
|||||||
HumanitecSyncSchema,
|
HumanitecSyncSchema,
|
||||||
TerraformCloudSyncSchema,
|
TerraformCloudSyncSchema,
|
||||||
CamundaSyncSchema,
|
CamundaSyncSchema,
|
||||||
VercelSyncSchema
|
VercelSyncSchema,
|
||||||
|
WindmillSyncSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||||
@@ -51,7 +53,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
|||||||
HumanitecSyncListItemSchema,
|
HumanitecSyncListItemSchema,
|
||||||
TerraformCloudSyncListItemSchema,
|
TerraformCloudSyncListItemSchema,
|
||||||
CamundaSyncListItemSchema,
|
CamundaSyncListItemSchema,
|
||||||
VercelSyncListItemSchema
|
VercelSyncListItemSchema,
|
||||||
|
WindmillSyncListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import {
|
||||||
|
CreateWindmillSyncSchema,
|
||||||
|
UpdateWindmillSyncSchema,
|
||||||
|
WindmillSyncSchema
|
||||||
|
} from "@app/services/secret-sync/windmill";
|
||||||
|
|
||||||
|
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||||
|
|
||||||
|
export const registerWindmillSyncRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSyncSecretsEndpoints({
|
||||||
|
destination: SecretSync.Windmill,
|
||||||
|
server,
|
||||||
|
responseSchema: WindmillSyncSchema,
|
||||||
|
createSchema: CreateWindmillSyncSchema,
|
||||||
|
updateSchema: UpdateWindmillSyncSchema
|
||||||
|
});
|
@@ -303,7 +303,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
|
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
|
||||||
description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription),
|
description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription),
|
||||||
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
|
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
|
||||||
|
hasDeleteProtection: z.boolean().optional().describe(PROJECTS.UPDATE.hasDeleteProtection)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: SanitizedProjectSchema
|
200: SanitizedProjectSchema
|
||||||
@@ -321,7 +322,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
update: {
|
update: {
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
description: req.body.description,
|
description: req.body.description,
|
||||||
autoCapitalization: req.body.autoCapitalization
|
autoCapitalization: req.body.autoCapitalization,
|
||||||
|
hasDeleteProtection: req.body.hasDeleteProtection
|
||||||
},
|
},
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
@@ -10,7 +10,9 @@ export enum AppConnection {
|
|||||||
Vercel = "vercel",
|
Vercel = "vercel",
|
||||||
Postgres = "postgres",
|
Postgres = "postgres",
|
||||||
MsSql = "mssql",
|
MsSql = "mssql",
|
||||||
Camunda = "camunda"
|
Camunda = "camunda",
|
||||||
|
Windmill = "windmill",
|
||||||
|
Auth0 = "auth0"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AWSRegion {
|
export enum AWSRegion {
|
||||||
|
@@ -16,6 +16,7 @@ import {
|
|||||||
TAppConnectionCredentialsValidator,
|
TAppConnectionCredentialsValidator,
|
||||||
TAppConnectionTransitionCredentialsToPlatform
|
TAppConnectionTransitionCredentialsToPlatform
|
||||||
} from "./app-connection-types";
|
} from "./app-connection-types";
|
||||||
|
import { Auth0ConnectionMethod, getAuth0ConnectionListItem, validateAuth0ConnectionCredentials } from "./auth0";
|
||||||
import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws";
|
import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws";
|
||||||
import {
|
import {
|
||||||
AzureAppConfigurationConnectionMethod,
|
AzureAppConfigurationConnectionMethod,
|
||||||
@@ -49,6 +50,11 @@ import {
|
|||||||
} from "./terraform-cloud";
|
} from "./terraform-cloud";
|
||||||
import { VercelConnectionMethod } from "./vercel";
|
import { VercelConnectionMethod } from "./vercel";
|
||||||
import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns";
|
import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns";
|
||||||
|
import {
|
||||||
|
getWindmillConnectionListItem,
|
||||||
|
validateWindmillConnectionCredentials,
|
||||||
|
WindmillConnectionMethod
|
||||||
|
} from "./windmill";
|
||||||
|
|
||||||
export const listAppConnectionOptions = () => {
|
export const listAppConnectionOptions = () => {
|
||||||
return [
|
return [
|
||||||
@@ -63,7 +69,9 @@ export const listAppConnectionOptions = () => {
|
|||||||
getVercelConnectionListItem(),
|
getVercelConnectionListItem(),
|
||||||
getPostgresConnectionListItem(),
|
getPostgresConnectionListItem(),
|
||||||
getMsSqlConnectionListItem(),
|
getMsSqlConnectionListItem(),
|
||||||
getCamundaConnectionListItem()
|
getCamundaConnectionListItem(),
|
||||||
|
getWindmillConnectionListItem(),
|
||||||
|
getAuth0ConnectionListItem()
|
||||||
].sort((a, b) => a.name.localeCompare(b.name));
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,25 +117,29 @@ export const decryptAppConnectionCredentials = async ({
|
|||||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
|
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
|
||||||
};
|
};
|
||||||
|
|
||||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
|
|
||||||
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
|
|
||||||
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
|
|
||||||
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
|
|
||||||
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
|
|
||||||
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
|
||||||
[AppConnection.AzureAppConfiguration]:
|
|
||||||
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
|
||||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
|
||||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
|
||||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
|
||||||
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
|
||||||
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
|
|
||||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateAppConnectionCredentials = async (
|
export const validateAppConnectionCredentials = async (
|
||||||
appConnection: TAppConnectionConfig
|
appConnection: TAppConnectionConfig
|
||||||
): Promise<TAppConnection["credentials"]> => VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
): Promise<TAppConnection["credentials"]> => {
|
||||||
|
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
|
||||||
|
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.AzureAppConfiguration]:
|
||||||
|
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator
|
||||||
|
};
|
||||||
|
|
||||||
|
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||||
|
};
|
||||||
|
|
||||||
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
|
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
@@ -154,6 +166,10 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case PostgresConnectionMethod.UsernameAndPassword:
|
case PostgresConnectionMethod.UsernameAndPassword:
|
||||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||||
return "Username & Password";
|
return "Username & Password";
|
||||||
|
case WindmillConnectionMethod.AccessToken:
|
||||||
|
return "Access Token";
|
||||||
|
case Auth0ConnectionMethod.ClientCredentials:
|
||||||
|
return "Client Credentials";
|
||||||
default:
|
default:
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
@@ -196,5 +212,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
|||||||
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||||
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
|
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported
|
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
|
||||||
|
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
|
||||||
|
[AppConnection.Auth0]: platformManagedCredentialsNotSupported
|
||||||
};
|
};
|
||||||
|
@@ -12,5 +12,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
|||||||
[AppConnection.Vercel]: "Vercel",
|
[AppConnection.Vercel]: "Vercel",
|
||||||
[AppConnection.Postgres]: "PostgreSQL",
|
[AppConnection.Postgres]: "PostgreSQL",
|
||||||
[AppConnection.MsSql]: "Microsoft SQL Server",
|
[AppConnection.MsSql]: "Microsoft SQL Server",
|
||||||
[AppConnection.Camunda]: "Camunda"
|
[AppConnection.Camunda]: "Camunda",
|
||||||
|
[AppConnection.Windmill]: "Windmill",
|
||||||
|
[AppConnection.Auth0]: "Auth0"
|
||||||
};
|
};
|
||||||
|
@@ -14,6 +14,7 @@ import {
|
|||||||
TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM,
|
TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM,
|
||||||
validateAppConnectionCredentials
|
validateAppConnectionCredentials
|
||||||
} from "@app/services/app-connection/app-connection-fns";
|
} from "@app/services/app-connection/app-connection-fns";
|
||||||
|
import { auth0ConnectionService } from "@app/services/app-connection/auth0/auth0-connection-service";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||||
@@ -27,6 +28,7 @@ import {
|
|||||||
TUpdateAppConnectionDTO,
|
TUpdateAppConnectionDTO,
|
||||||
TValidateAppConnectionCredentialsSchema
|
TValidateAppConnectionCredentialsSchema
|
||||||
} from "./app-connection-types";
|
} from "./app-connection-types";
|
||||||
|
import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0";
|
||||||
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||||
@@ -47,6 +49,8 @@ import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-c
|
|||||||
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
|
import { terraformCloudConnectionService } from "./terraform-cloud/terraform-cloud-connection-service";
|
||||||
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
|
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
|
||||||
import { vercelConnectionService } from "./vercel/vercel-connection-service";
|
import { vercelConnectionService } from "./vercel/vercel-connection-service";
|
||||||
|
import { ValidateWindmillConnectionCredentialsSchema } from "./windmill";
|
||||||
|
import { windmillConnectionService } from "./windmill/windmill-connection-service";
|
||||||
|
|
||||||
export type TAppConnectionServiceFactoryDep = {
|
export type TAppConnectionServiceFactoryDep = {
|
||||||
appConnectionDAL: TAppConnectionDALFactory;
|
appConnectionDAL: TAppConnectionDALFactory;
|
||||||
@@ -68,7 +72,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
|||||||
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema,
|
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema,
|
||||||
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
|
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
|
||||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
||||||
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema
|
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
|
||||||
|
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
|
||||||
|
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appConnectionServiceFactory = ({
|
export const appConnectionServiceFactory = ({
|
||||||
@@ -442,6 +448,8 @@ export const appConnectionServiceFactory = ({
|
|||||||
humanitec: humanitecConnectionService(connectAppConnectionById),
|
humanitec: humanitecConnectionService(connectAppConnectionById),
|
||||||
terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
|
terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
|
||||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
vercel: vercelConnectionService(connectAppConnectionById)
|
vercel: vercelConnectionService(connectAppConnectionById),
|
||||||
|
windmill: windmillConnectionService(connectAppConnectionById),
|
||||||
|
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -3,6 +3,12 @@ import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sq
|
|||||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
import { AWSRegion } from "./app-connection-enums";
|
import { AWSRegion } from "./app-connection-enums";
|
||||||
|
import {
|
||||||
|
TAuth0Connection,
|
||||||
|
TAuth0ConnectionConfig,
|
||||||
|
TAuth0ConnectionInput,
|
||||||
|
TValidateAuth0ConnectionCredentialsSchema
|
||||||
|
} from "./auth0";
|
||||||
import {
|
import {
|
||||||
TAwsConnection,
|
TAwsConnection,
|
||||||
TAwsConnectionConfig,
|
TAwsConnectionConfig,
|
||||||
@@ -69,6 +75,12 @@ import {
|
|||||||
TVercelConnectionConfig,
|
TVercelConnectionConfig,
|
||||||
TVercelConnectionInput
|
TVercelConnectionInput
|
||||||
} from "./vercel";
|
} from "./vercel";
|
||||||
|
import {
|
||||||
|
TValidateWindmillConnectionCredentialsSchema,
|
||||||
|
TWindmillConnection,
|
||||||
|
TWindmillConnectionConfig,
|
||||||
|
TWindmillConnectionInput
|
||||||
|
} from "./windmill";
|
||||||
|
|
||||||
export type TAppConnection = { id: string } & (
|
export type TAppConnection = { id: string } & (
|
||||||
| TAwsConnection
|
| TAwsConnection
|
||||||
@@ -83,6 +95,8 @@ export type TAppConnection = { id: string } & (
|
|||||||
| TPostgresConnection
|
| TPostgresConnection
|
||||||
| TMsSqlConnection
|
| TMsSqlConnection
|
||||||
| TCamundaConnection
|
| TCamundaConnection
|
||||||
|
| TWindmillConnection
|
||||||
|
| TAuth0Connection
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||||
@@ -102,6 +116,8 @@ export type TAppConnectionInput = { id: string } & (
|
|||||||
| TPostgresConnectionInput
|
| TPostgresConnectionInput
|
||||||
| TMsSqlConnectionInput
|
| TMsSqlConnectionInput
|
||||||
| TCamundaConnectionInput
|
| TCamundaConnectionInput
|
||||||
|
| TWindmillConnectionInput
|
||||||
|
| TAuth0ConnectionInput
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
|
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
|
||||||
@@ -126,7 +142,9 @@ export type TAppConnectionConfig =
|
|||||||
| TTerraformCloudConnectionConfig
|
| TTerraformCloudConnectionConfig
|
||||||
| TVercelConnectionConfig
|
| TVercelConnectionConfig
|
||||||
| TSqlConnectionConfig
|
| TSqlConnectionConfig
|
||||||
| TCamundaConnectionConfig;
|
| TCamundaConnectionConfig
|
||||||
|
| TWindmillConnectionConfig
|
||||||
|
| TAuth0ConnectionConfig;
|
||||||
|
|
||||||
export type TValidateAppConnectionCredentialsSchema =
|
export type TValidateAppConnectionCredentialsSchema =
|
||||||
| TValidateAwsConnectionCredentialsSchema
|
| TValidateAwsConnectionCredentialsSchema
|
||||||
@@ -140,7 +158,9 @@ export type TValidateAppConnectionCredentialsSchema =
|
|||||||
| TValidateMsSqlConnectionCredentialsSchema
|
| TValidateMsSqlConnectionCredentialsSchema
|
||||||
| TValidateCamundaConnectionCredentialsSchema
|
| TValidateCamundaConnectionCredentialsSchema
|
||||||
| TValidateTerraformCloudConnectionCredentialsSchema
|
| TValidateTerraformCloudConnectionCredentialsSchema
|
||||||
| TValidateVercelConnectionCredentialsSchema;
|
| TValidateVercelConnectionCredentialsSchema
|
||||||
|
| TValidateWindmillConnectionCredentialsSchema
|
||||||
|
| TValidateAuth0ConnectionCredentialsSchema;
|
||||||
|
|
||||||
export type TListAwsConnectionKmsKeys = {
|
export type TListAwsConnectionKmsKeys = {
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
export enum Auth0ConnectionMethod {
|
||||||
|
ClientCredentials = "client-credentials"
|
||||||
|
}
|
@@ -0,0 +1,97 @@
|
|||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { Auth0ConnectionMethod } from "./auth0-connection-enums";
|
||||||
|
import { TAuth0AccessTokenResponse, TAuth0Connection, TAuth0ConnectionConfig } from "./auth0-connection-types";
|
||||||
|
|
||||||
|
export const getAuth0ConnectionListItem = () => {
|
||||||
|
return {
|
||||||
|
name: "Auth0" as const,
|
||||||
|
app: AppConnection.Auth0 as const,
|
||||||
|
methods: Object.values(Auth0ConnectionMethod) as [Auth0ConnectionMethod.ClientCredentials]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const authorizeAuth0Connection = async ({
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
domain,
|
||||||
|
audience
|
||||||
|
}: TAuth0ConnectionConfig["credentials"]) => {
|
||||||
|
const instanceUrl = domain.startsWith("http") ? domain : `https://${domain}`;
|
||||||
|
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||||
|
|
||||||
|
const { data } = await request.request<TAuth0AccessTokenResponse>({
|
||||||
|
method: "POST",
|
||||||
|
url: `${removeTrailingSlash(instanceUrl)}/oauth/token`,
|
||||||
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||||
|
data: new URLSearchParams({
|
||||||
|
grant_type: "client_credentials", // this will need to be resolved if we support methods other than client credentials
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
audience
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.token_type !== "Bearer") {
|
||||||
|
throw new Error(`Unhandled token type: ${data.token_type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: data.access_token,
|
||||||
|
// cap token lifespan to 10 minutes
|
||||||
|
expiresAt: Math.min(data.expires_in * 1000, 600000) + Date.now()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAuth0ConnectionAccessToken = async (
|
||||||
|
{ id, orgId, credentials }: TAuth0Connection,
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
) => {
|
||||||
|
const { expiresAt, accessToken } = credentials;
|
||||||
|
|
||||||
|
// get new token if expired or less than 5 minutes until expiry
|
||||||
|
if (Date.now() < expiresAt - 300000) {
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authData = await authorizeAuth0Connection(credentials);
|
||||||
|
|
||||||
|
const updatedCredentials: TAuth0Connection["credentials"] = {
|
||||||
|
...credentials,
|
||||||
|
...authData
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: updatedCredentials,
|
||||||
|
orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
await appConnectionDAL.updateById(id, { encryptedCredentials });
|
||||||
|
|
||||||
|
return authData.accessToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateAuth0ConnectionCredentials = async ({ credentials }: TAuth0ConnectionConfig) => {
|
||||||
|
try {
|
||||||
|
const { accessToken, expiresAt } = await authorizeAuth0Connection(credentials);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...credentials,
|
||||||
|
accessToken,
|
||||||
|
expiresAt
|
||||||
|
};
|
||||||
|
} catch (e: unknown) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: (e as Error).message ?? `Unable to validate connection: verify credentials`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1,94 @@
|
|||||||
|
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 { Auth0ConnectionMethod } from "./auth0-connection-enums";
|
||||||
|
|
||||||
|
export const Auth0ConnectionClientCredentialsInputCredentialsSchema = z.object({
|
||||||
|
domain: z.string().trim().min(1, "Domain required").describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.domain),
|
||||||
|
clientId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Client ID required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientId),
|
||||||
|
clientSecret: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Client Secret required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.clientSecret),
|
||||||
|
audience: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.url()
|
||||||
|
.min(1, "Audience required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.AUTH0_CONNECTION.audience)
|
||||||
|
});
|
||||||
|
|
||||||
|
const Auth0ConnectionClientCredentialsOutputCredentialsSchema = z
|
||||||
|
.object({
|
||||||
|
accessToken: z.string(),
|
||||||
|
expiresAt: z.number()
|
||||||
|
})
|
||||||
|
.merge(Auth0ConnectionClientCredentialsInputCredentialsSchema);
|
||||||
|
|
||||||
|
const BaseAuth0ConnectionSchema = BaseAppConnectionSchema.extend({
|
||||||
|
app: z.literal(AppConnection.Auth0)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Auth0ConnectionSchema = z.intersection(
|
||||||
|
BaseAuth0ConnectionSchema,
|
||||||
|
z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(Auth0ConnectionMethod.ClientCredentials),
|
||||||
|
credentials: Auth0ConnectionClientCredentialsOutputCredentialsSchema
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SanitizedAuth0ConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseAuth0ConnectionSchema.extend({
|
||||||
|
method: z.literal(Auth0ConnectionMethod.ClientCredentials),
|
||||||
|
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.pick({
|
||||||
|
domain: true,
|
||||||
|
clientId: true,
|
||||||
|
audience: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ValidateAuth0ConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(Auth0ConnectionMethod.ClientCredentials)
|
||||||
|
.describe(AppConnections.CREATE(AppConnection.Auth0).method),
|
||||||
|
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.Auth0).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateAuth0ConnectionSchema = ValidateAuth0ConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.Auth0)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateAuth0ConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: Auth0ConnectionClientCredentialsInputCredentialsSchema.optional().describe(
|
||||||
|
AppConnections.UPDATE(AppConnection.Auth0).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Auth0));
|
||||||
|
|
||||||
|
export const Auth0ConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("Auth0"),
|
||||||
|
app: z.literal(AppConnection.Auth0),
|
||||||
|
// the below is preferable but currently breaks with our zod to json schema parser
|
||||||
|
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
|
||||||
|
methods: z.nativeEnum(Auth0ConnectionMethod).array()
|
||||||
|
});
|
@@ -0,0 +1,71 @@
|
|||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { getAuth0ConnectionAccessToken } from "@app/services/app-connection/auth0/auth0-connection-fns";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { TAuth0Connection, TAuth0ListClient, TAuth0ListClientsResponse } from "./auth0-connection-types";
|
||||||
|
|
||||||
|
type TGetAppConnectionFunc = (
|
||||||
|
app: AppConnection,
|
||||||
|
connectionId: string,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => Promise<TAuth0Connection>;
|
||||||
|
|
||||||
|
const listAuth0Clients = async (
|
||||||
|
appConnection: TAuth0Connection,
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
) => {
|
||||||
|
const accessToken = await getAuth0ConnectionAccessToken(appConnection, appConnectionDAL, kmsService);
|
||||||
|
|
||||||
|
const { audience, clientId: connectionClientId } = appConnection.credentials;
|
||||||
|
await blockLocalAndPrivateIpAddresses(audience);
|
||||||
|
|
||||||
|
const clients: TAuth0ListClient[] = [];
|
||||||
|
let hasMore = true;
|
||||||
|
let page = 0;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const { data: clientsPage } = await request.get<TAuth0ListClientsResponse>(`${audience}clients`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Accept-Encoding": "application/json"
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
include_totals: true,
|
||||||
|
per_page: 100,
|
||||||
|
page
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clients.push(...clientsPage.clients);
|
||||||
|
page += 1;
|
||||||
|
hasMore = clientsPage.total > clients.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
clients.filter((client) => client.client_id !== connectionClientId && client.name !== "All Applications") ?? []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const auth0ConnectionService = (
|
||||||
|
getAppConnection: TGetAppConnectionFunc,
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
) => {
|
||||||
|
const listClients = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.Auth0, connectionId, actor);
|
||||||
|
|
||||||
|
const clients = await listAuth0Clients(appConnection, appConnectionDAL, kmsService);
|
||||||
|
|
||||||
|
return clients.map((client) => ({ id: client.client_id, name: client.name }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listClients
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,39 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Auth0ConnectionSchema,
|
||||||
|
CreateAuth0ConnectionSchema,
|
||||||
|
ValidateAuth0ConnectionCredentialsSchema
|
||||||
|
} from "./auth0-connection-schemas";
|
||||||
|
|
||||||
|
export type TAuth0Connection = z.infer<typeof Auth0ConnectionSchema>;
|
||||||
|
|
||||||
|
export type TAuth0ConnectionInput = z.infer<typeof CreateAuth0ConnectionSchema> & {
|
||||||
|
app: AppConnection.Auth0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateAuth0ConnectionCredentialsSchema = typeof ValidateAuth0ConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TAuth0ConnectionConfig = DiscriminativePick<TAuth0Connection, "method" | "app" | "credentials"> & {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAuth0AccessTokenResponse = {
|
||||||
|
access_token: string;
|
||||||
|
expires_in: number;
|
||||||
|
scope: string;
|
||||||
|
token_type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAuth0ListClient = {
|
||||||
|
name: string;
|
||||||
|
client_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAuth0ListClientsResponse = {
|
||||||
|
total: number;
|
||||||
|
clients: TAuth0ListClient[];
|
||||||
|
};
|
4
backend/src/services/app-connection/auth0/index.ts
Normal file
4
backend/src/services/app-connection/auth0/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./auth0-connection-enums";
|
||||||
|
export * from "./auth0-connection-fns";
|
||||||
|
export * from "./auth0-connection-schemas";
|
||||||
|
export * from "./auth0-connection-types";
|
@@ -1,6 +1,7 @@
|
|||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||||
@@ -26,6 +27,8 @@ const authorizeDatabricksConnection = async ({
|
|||||||
clientSecret,
|
clientSecret,
|
||||||
workspaceUrl
|
workspaceUrl
|
||||||
}: Pick<TDatabricksConnection["credentials"], "workspaceUrl" | "clientId" | "clientSecret">) => {
|
}: Pick<TDatabricksConnection["credentials"], "workspaceUrl" | "clientId" | "clientSecret">) => {
|
||||||
|
await blockLocalAndPrivateIpAddresses(workspaceUrl);
|
||||||
|
|
||||||
const { data } = await request.post<TAuthorizeDatabricksConnection>(
|
const { data } = await request.post<TAuthorizeDatabricksConnection>(
|
||||||
`${removeTrailingSlash(workspaceUrl)}/oidc/v1/token`,
|
`${removeTrailingSlash(workspaceUrl)}/oidc/v1/token`,
|
||||||
"grant_type=client_credentials&scope=all-apis",
|
"grant_type=client_credentials&scope=all-apis",
|
||||||
|
@@ -16,7 +16,7 @@ export const DatabricksConnectionServicePrincipalInputCredentialsSchema = z.obje
|
|||||||
workspaceUrl: z.string().trim().url().min(1, "Workspace URL required")
|
workspaceUrl: z.string().trim().url().min(1, "Workspace URL required")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z
|
const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z
|
||||||
.object({
|
.object({
|
||||||
accessToken: z.string(),
|
accessToken: z.string(),
|
||||||
expiresAt: z.number()
|
expiresAt: z.number()
|
||||||
|
@@ -44,7 +44,7 @@ export const validateTerraformCloudConnectionCredentials = async (config: TTerra
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Unable to validate connection - verify credentials"
|
message: "Unable to validate connection: verify credentials"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import { AxiosError, AxiosResponse } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
@@ -27,10 +27,8 @@ export const getVercelConnectionListItem = () => {
|
|||||||
export const validateVercelConnectionCredentials = async (config: TVercelConnectionConfig) => {
|
export const validateVercelConnectionCredentials = async (config: TVercelConnectionConfig) => {
|
||||||
const { credentials: inputCredentials } = config;
|
const { credentials: inputCredentials } = config;
|
||||||
|
|
||||||
let response: AxiosResponse<VercelApp[]> | null = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = await request.get<VercelApp[]>(`${IntegrationUrls.VERCEL_API_URL}/v9/projects`, {
|
await request.get(`${IntegrationUrls.VERCEL_API_URL}/v2/user`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${inputCredentials.apiToken}`
|
Authorization: `Bearer ${inputCredentials.apiToken}`
|
||||||
}
|
}
|
||||||
@@ -38,17 +36,14 @@ export const validateVercelConnectionCredentials = async (config: TVercelConnect
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof AxiosError) {
|
if (error instanceof AxiosError) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Failed to validate credentials: ${error.message || "Unknown error"}`
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
message: `Failed to validate credentials: ${
|
||||||
|
error.response?.data ? JSON.stringify(error.response?.data) : error.message || "Unknown error"
|
||||||
|
}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Unable to validate connection - verify credentials"
|
message: `Unable to validate connection: ${(error as Error).message || "Verify credentials"}`
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response?.data) {
|
|
||||||
throw new InternalServerError({
|
|
||||||
message: "Failed to get organizations: Response was empty"
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
backend/src/services/app-connection/windmill/index.ts
Normal file
4
backend/src/services/app-connection/windmill/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./windmill-connection-enums";
|
||||||
|
export * from "./windmill-connection-fns";
|
||||||
|
export * from "./windmill-connection-schemas";
|
||||||
|
export * from "./windmill-connection-types";
|
@@ -0,0 +1,3 @@
|
|||||||
|
export enum WindmillConnectionMethod {
|
||||||
|
AccessToken = "access-token"
|
||||||
|
}
|
@@ -0,0 +1,65 @@
|
|||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
import { WindmillConnectionMethod } from "./windmill-connection-enums";
|
||||||
|
import { TWindmillConnection, TWindmillConnectionConfig, TWindmillWorkspace } from "./windmill-connection-types";
|
||||||
|
|
||||||
|
export const getWindmillInstanceUrl = async (config: TWindmillConnectionConfig) => {
|
||||||
|
const instanceUrl = config.credentials.instanceUrl
|
||||||
|
? removeTrailingSlash(config.credentials.instanceUrl)
|
||||||
|
: "https://app.windmill.dev";
|
||||||
|
|
||||||
|
await blockLocalAndPrivateIpAddresses(instanceUrl);
|
||||||
|
|
||||||
|
return instanceUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWindmillConnectionListItem = () => {
|
||||||
|
return {
|
||||||
|
name: "Windmill" as const,
|
||||||
|
app: AppConnection.Windmill as const,
|
||||||
|
methods: Object.values(WindmillConnectionMethod) as [WindmillConnectionMethod.AccessToken]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateWindmillConnectionCredentials = async (config: TWindmillConnectionConfig) => {
|
||||||
|
const instanceUrl = await getWindmillInstanceUrl(config);
|
||||||
|
const { accessToken } = config.credentials;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await request.get(`${instanceUrl}/api/workspaces/list`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
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 listWindmillWorkspaces = async (appConnection: TWindmillConnection) => {
|
||||||
|
const instanceUrl = await getWindmillInstanceUrl(appConnection);
|
||||||
|
const { accessToken } = appConnection.credentials;
|
||||||
|
|
||||||
|
const resp = await request.get<TWindmillWorkspace[]>(`${instanceUrl}/api/workspaces/list`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return resp.data.filter((workspace) => !workspace.deleted);
|
||||||
|
};
|
@@ -0,0 +1,70 @@
|
|||||||
|
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 { WindmillConnectionMethod } from "./windmill-connection-enums";
|
||||||
|
|
||||||
|
export const WindmillConnectionAccessTokenCredentialsSchema = z.object({
|
||||||
|
accessToken: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Access Token required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.WINDMILL.accessToken),
|
||||||
|
instanceUrl: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.url("Invalid Instance URL")
|
||||||
|
.optional()
|
||||||
|
.describe(AppConnections.CREDENTIALS.WINDMILL.instanceUrl)
|
||||||
|
});
|
||||||
|
|
||||||
|
const BaseWindmillConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Windmill) });
|
||||||
|
|
||||||
|
export const WindmillConnectionSchema = BaseWindmillConnectionSchema.extend({
|
||||||
|
method: z.literal(WindmillConnectionMethod.AccessToken),
|
||||||
|
credentials: WindmillConnectionAccessTokenCredentialsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SanitizedWindmillConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseWindmillConnectionSchema.extend({
|
||||||
|
method: z.literal(WindmillConnectionMethod.AccessToken),
|
||||||
|
credentials: WindmillConnectionAccessTokenCredentialsSchema.pick({
|
||||||
|
instanceUrl: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ValidateWindmillConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(WindmillConnectionMethod.AccessToken)
|
||||||
|
.describe(AppConnections.CREATE(AppConnection.Windmill).method),
|
||||||
|
credentials: WindmillConnectionAccessTokenCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.Windmill).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateWindmillConnectionSchema = ValidateWindmillConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.Windmill)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateWindmillConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: WindmillConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||||
|
AppConnections.UPDATE(AppConnection.Windmill).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Windmill));
|
||||||
|
|
||||||
|
export const WindmillConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("Windmill"),
|
||||||
|
app: z.literal(AppConnection.Windmill),
|
||||||
|
methods: z.nativeEnum(WindmillConnectionMethod).array()
|
||||||
|
});
|
@@ -0,0 +1,28 @@
|
|||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import { listWindmillWorkspaces } from "./windmill-connection-fns";
|
||||||
|
import { TWindmillConnection } from "./windmill-connection-types";
|
||||||
|
|
||||||
|
type TGetAppConnectionFunc = (
|
||||||
|
app: AppConnection,
|
||||||
|
connectionId: string,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => Promise<TWindmillConnection>;
|
||||||
|
|
||||||
|
export const windmillConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||||
|
const listWorkspaces = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.Windmill, connectionId, actor);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workspaces = await listWindmillWorkspaces(appConnection);
|
||||||
|
return workspaces;
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listWorkspaces
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,27 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import {
|
||||||
|
CreateWindmillConnectionSchema,
|
||||||
|
ValidateWindmillConnectionCredentialsSchema,
|
||||||
|
WindmillConnectionSchema
|
||||||
|
} from "./windmill-connection-schemas";
|
||||||
|
|
||||||
|
export type TWindmillConnection = z.infer<typeof WindmillConnectionSchema>;
|
||||||
|
|
||||||
|
export type TWindmillConnectionInput = z.infer<typeof CreateWindmillConnectionSchema> & {
|
||||||
|
app: AppConnection.Windmill;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateWindmillConnectionCredentialsSchema = typeof ValidateWindmillConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TWindmillConnectionConfig = DiscriminativePick<
|
||||||
|
TWindmillConnectionInput,
|
||||||
|
"method" | "app" | "credentials"
|
||||||
|
> & {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TWindmillWorkspace = { id: string; name: string; deleted: boolean };
|
@@ -2,7 +2,7 @@ import bcrypt from "bcrypt";
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
import { OrgMembershipRole, TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
@@ -174,20 +174,25 @@ export const authLoginServiceFactory = ({
|
|||||||
const userEnc = await userDAL.findUserEncKeyByUsername({
|
const userEnc = await userDAL.findUserEncKeyByUsername({
|
||||||
username: email
|
username: email
|
||||||
});
|
});
|
||||||
|
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
|
|
||||||
|
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
|
||||||
|
throw new Error("Failed to find user");
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
serverCfg.enabledLoginMethods &&
|
serverCfg.enabledLoginMethods &&
|
||||||
!serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) &&
|
!serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) &&
|
||||||
!providerAuthToken
|
!providerAuthToken
|
||||||
) {
|
) {
|
||||||
throw new BadRequestError({
|
// bypass server configuration when user is an organization admin - this is to prevent lockout
|
||||||
message: "Login with email is disabled by administrator."
|
const userOrgs = await orgDAL.findAllOrgsByUserId(userEnc.userId);
|
||||||
});
|
if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
|
||||||
}
|
throw new BadRequestError({
|
||||||
|
message: "Login with email is disabled by administrator."
|
||||||
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
|
});
|
||||||
throw new Error("Failed to find user");
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
|
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
|
||||||
@@ -573,28 +578,40 @@ export const authLoginServiceFactory = ({
|
|||||||
switch (authMethod) {
|
switch (authMethod) {
|
||||||
case AuthMethod.GITHUB: {
|
case AuthMethod.GITHUB: {
|
||||||
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) {
|
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITHUB)) {
|
||||||
throw new BadRequestError({
|
// bypass server configuration when user is an organization admin - this is to prevent lockout
|
||||||
message: "Login with Github is disabled by administrator.",
|
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||||
name: "Oauth 2 login"
|
if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
|
||||||
});
|
throw new BadRequestError({
|
||||||
|
message: "Login with Github is disabled by administrator.",
|
||||||
|
name: "Oauth 2 login"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AuthMethod.GOOGLE: {
|
case AuthMethod.GOOGLE: {
|
||||||
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) {
|
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) {
|
||||||
throw new BadRequestError({
|
// bypass server configuration when user is an organization admin - this is to prevent lockout
|
||||||
message: "Login with Google is disabled by administrator.",
|
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||||
name: "Oauth 2 login"
|
if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
|
||||||
});
|
throw new BadRequestError({
|
||||||
|
message: "Login with Google is disabled by administrator.",
|
||||||
|
name: "Oauth 2 login"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AuthMethod.GITLAB: {
|
case AuthMethod.GITLAB: {
|
||||||
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) {
|
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) {
|
||||||
throw new BadRequestError({
|
// bypass server configuration when user is an organization admin - this is to prevent lockout
|
||||||
message: "Login with Gitlab is disabled by administrator.",
|
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||||
name: "Oauth 2 login"
|
if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
|
||||||
});
|
throw new BadRequestError({
|
||||||
|
message: "Login with Gitlab is disabled by administrator.",
|
||||||
|
name: "Oauth 2 login"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@@ -195,6 +195,7 @@ export const getIntegrationOptions = async () => {
|
|||||||
{
|
{
|
||||||
name: "AWS Secrets Manager",
|
name: "AWS Secrets Manager",
|
||||||
slug: "aws-secret-manager",
|
slug: "aws-secret-manager",
|
||||||
|
syncSlug: "aws-secrets-manager",
|
||||||
image: "Amazon Web Services.png",
|
image: "Amazon Web Services.png",
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
type: "custom",
|
type: "custom",
|
||||||
|
@@ -288,11 +288,6 @@ export const kmsServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
|
|
||||||
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
|
||||||
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
|
|
||||||
});
|
|
||||||
|
|
||||||
if (kmsDoc.externalKms) {
|
if (kmsDoc.externalKms) {
|
||||||
let externalKms: TExternalKmsProviderFns;
|
let externalKms: TExternalKmsProviderFns;
|
||||||
|
|
||||||
@@ -353,6 +348,11 @@ export const kmsServiceFactory = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
|
||||||
|
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||||
|
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
|
||||||
|
});
|
||||||
|
|
||||||
// internal KMS
|
// internal KMS
|
||||||
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
||||||
@@ -509,11 +509,6 @@ export const kmsServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
throw new NotFoundError({ message: `KMS with ID '${kmsId}' not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
|
|
||||||
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
|
||||||
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
|
|
||||||
});
|
|
||||||
|
|
||||||
if (kmsDoc.externalKms) {
|
if (kmsDoc.externalKms) {
|
||||||
let externalKms: TExternalKmsProviderFns;
|
let externalKms: TExternalKmsProviderFns;
|
||||||
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
|
if (!kmsDoc.orgKms.id || !kmsDoc.orgKms.encryptedDataKey) {
|
||||||
@@ -568,6 +563,11 @@ export const kmsServiceFactory = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const encryptionAlgorithm = kmsDoc.internalKms?.encryptionAlgorithm as SymmetricKeyAlgorithm;
|
||||||
|
verifyKeyTypeAndAlgorithm(kmsDoc.keyUsage as KmsKeyUsage, encryptionAlgorithm, {
|
||||||
|
forceType: KmsKeyUsage.ENCRYPT_DECRYPT
|
||||||
|
});
|
||||||
|
|
||||||
// internal KMS
|
// internal KMS
|
||||||
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
const keyCipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
|
||||||
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
const dataCipher = symmetricCipherService(encryptionAlgorithm);
|
||||||
|
@@ -96,7 +96,9 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// special query
|
// special query
|
||||||
const findAllOrgsByUserId = async (userId: string): Promise<(TOrganizations & { orgAuthMethod: string })[]> => {
|
const findAllOrgsByUserId = async (
|
||||||
|
userId: string
|
||||||
|
): Promise<(TOrganizations & { orgAuthMethod: string; userRole: string })[]> => {
|
||||||
try {
|
try {
|
||||||
const org = (await db
|
const org = (await db
|
||||||
.replicaNode()(TableName.OrgMembership)
|
.replicaNode()(TableName.OrgMembership)
|
||||||
@@ -117,6 +119,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.select(selectAllTableCols(TableName.Organization))
|
.select(selectAllTableCols(TableName.Organization))
|
||||||
|
.select(db.ref("role").withSchema(TableName.OrgMembership).as("userRole"))
|
||||||
.select(
|
.select(
|
||||||
db.raw(`
|
db.raw(`
|
||||||
CASE
|
CASE
|
||||||
@@ -125,7 +128,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
|||||||
ELSE ''
|
ELSE ''
|
||||||
END as "orgAuthMethod"
|
END as "orgAuthMethod"
|
||||||
`)
|
`)
|
||||||
)) as (TOrganizations & { orgAuthMethod: string })[];
|
)) as (TOrganizations & { orgAuthMethod: string; userRole: string })[];
|
||||||
|
|
||||||
return org;
|
return org;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@@ -16,5 +16,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
|||||||
allowSecretSharingOutsideOrganization: true,
|
allowSecretSharingOutsideOrganization: true,
|
||||||
shouldUseNewPrivilegeSystem: true,
|
shouldUseNewPrivilegeSystem: true,
|
||||||
privilegeUpgradeInitiatedByUsername: true,
|
privilegeUpgradeInitiatedByUsername: true,
|
||||||
privilegeUpgradeInitiatedAt: true
|
privilegeUpgradeInitiatedAt: true,
|
||||||
|
bypassOrgAuthEnabled: true
|
||||||
});
|
});
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user