Compare commits

..

67 Commits

Author SHA1 Message Date
7655dc7f3c chore: remove unused type 2025-04-17 12:22:02 -07:00
6c6c4db92c fix: use a non-team based api call for validation 2025-04-17 12:20:02 -07:00
8cf125ed32 Merge pull request #3444 from Infisical/default-project-delete-protection-false
Improvement: Set Project Delete Protection to False by Default
2025-04-17 12:21:46 -04:00
886cc9a113 improvement: set project delete protection to false by default 2025-04-17 09:16:36 -07:00
e1016f0a8b Merge pull request #3441 from Infisical/revert-3439-daniel/remove-docs
Revert "fix: removed legacy sdk's"
2025-04-17 07:05:30 +04:00
9c0a5f0bd4 fix: deprecation notices 2025-04-17 07:03:20 +04:00
7facd0e89e Revert "fix: removed legacy sdk's" 2025-04-17 05:52:07 +04:00
ee185cbe47 Merge pull request #3425 from akhilmhdh/feat/aws-cf-invalidate
Added aws cf invalidation on cli deployment pipeline
2025-04-16 17:22:10 -04:00
abc2f3808e Merge pull request #3438 from akhilmhdh/doc/sql-change
Updated doc on db permission change
2025-04-16 17:15:57 -04:00
733440a7b5 update docs for pg permissions 2025-04-16 17:15:11 -04:00
1ef3525917 Merge pull request #3439 from Infisical/daniel/remove-docs
fix: removed legacy sdk's
2025-04-16 16:42:58 -04:00
6664add428 fix: removed legacy sdk's 2025-04-17 00:41:29 +04:00
242e8fd2c6 Merge pull request #3424 from Infisical/misc/allow-org-admins-to-bypass-sso-enforcement
misc: allow org admins to bypass sso enforcement
2025-04-17 00:22:18 +04:00
1137247e69 misc: addressed feedback 2025-04-17 04:00:02 +08:00
=
32b951f6e9 doc: updated doc on db permission change 2025-04-17 01:09:23 +05:30
6f5fe053cd Merge pull request #3422 from Infisical/feat/addProjectDeletionProtection
Add project delete protection
2025-04-16 16:30:46 -03:00
17233e6a6f Merge pull request #3437 from Infisical/feat/addDocsOnSamlModal
Add SAML doc links to Org Settings
2025-04-16 14:20:28 -03:00
0dd06c1d66 Merge pull request #3419 from Infisical/feat/notifyOnServiceTokenExpiration
Add notification on Service Token expiration
2025-04-16 14:20:09 -03:00
fc2e5d18b7 misc: displayed full admin login url 2025-04-17 00:52:24 +08:00
ae1ee25687 Merge pull request #3436 from Infisical/misc/made-jwt-signature-alg-configurable-for-oidc
misc: made jwt signature alg configurable for oidc
2025-04-17 00:47:16 +08:00
5d0bbce12d misc: added admin login url to tooltip 2025-04-17 00:41:42 +08:00
8c87c40467 misc: only bypass when from admin login 2025-04-17 00:33:07 +08:00
a9dab557d9 misc: correct labels 2025-04-17 00:06:27 +08:00
76c3f1c152 misc: made bypass opt-in 2025-04-16 23:58:20 +08:00
965084cc0c notifyExpiredTokens fixes 2025-04-16 12:48:00 -03:00
4650ba9fdd Merge pull request #3397 from Infisical/auth0-connection-and-secret-rotation
Feature: Auth0 Connection and Client Secret Rotation
2025-04-16 08:19:50 -07:00
73dea6a0be Merge branch 'main' into feat/addProjectDeletionProtection 2025-04-16 10:00:23 -03:00
e7742afcd3 Merge pull request #3434 from Infisical/fix/improveRandomValueGeneratorUI
Improve random value generator modal UI
2025-04-16 09:58:07 -03:00
7d3dd765ad Add SAML doc links to Org Settings 2025-04-16 09:52:58 -03:00
927eb0407d misc: update documentation 2025-04-16 12:33:22 +00:00
17ddb79def misc: made jwt signature alg configurable for oidc 2025-04-16 20:20:37 +08:00
5ef5a5a107 Improve random value generator modal UI 2025-04-16 08:04:41 -03:00
9ae0880f50 Improve random value generator modal UI 2025-04-16 07:07:37 -03:00
3814c65f38 Merge pull request #3433 from akhilmhdh/fix/permission
feat: dashboard failing on failing check
2025-04-16 13:00:43 +05:30
3fa98e2a8d Merge pull request #3428 from x032205/cli-secrets-folders-get-path
Fixed `v1/folders` API backward compatibility with `directory` parameter
2025-04-16 12:49:02 +05:30
=
c6b21491db feat: dashboard failing on failing check 2025-04-16 12:37:39 +05:30
82af77c480 Add hasDeleteProtection to update endpoint 2025-04-15 22:37:53 -03:00
x
b2fae5c439 fixed backward compatibility with --directory flag on every v1/folders endpoint 2025-04-15 18:46:08 -04:00
x
f16e96759f fixed --path not working with infisical secrets folders get 2025-04-15 18:42:01 -04:00
5eb9a1a667 improvement: add doc additions for single credential rotations 2025-04-15 15:07:39 -07:00
03ad6f822a merge deconflict 2025-04-15 14:32:21 -07:00
23a5a7a624 Improvements on notify expired service tokens 2025-04-15 18:31:05 -03:00
98447e9402 improvements: address feedback 2025-04-15 14:22:41 -07:00
0f7e8585dc Merge pull request #3391 from Infisical/feat/add-metadata-based-permissions-for-dynamic-secret
feat: add metadata based permissions for dynamic secret
2025-04-16 04:28:52 +08:00
8568d1f6fe Merge pull request #3426 from Infisical/fix/addConfusedDeputyProblemOnAWSDocs
Add confused deputy problem to AWS assume role docs
2025-04-15 17:03:04 -03:00
27198869d8 Confused Deputy Attacks docs improvement 2025-04-15 16:55:05 -03:00
dd0880825b doc: added reference to admin login portal 2025-04-15 19:50:49 +00:00
f27050a1c3 Merge pull request #3421 from x032205/san-size-limit
Increase Certificate Alternative Names (SAN) Character Limit to 4096
2025-04-15 15:29:58 -04:00
785173747f misc: introduce admin login portal 2025-04-16 03:28:04 +08:00
d33b06dd8a Add confused deputy problem to AWS assume role docs 2025-04-15 15:41:13 -03:00
=
9a6e27d4be feat: added aws cf invalidation on cli deployment pipeline 2025-04-15 23:41:06 +05:30
d0db5c00e8 misc: allow org admins to bypass sso enforcement 2025-04-16 01:35:39 +08:00
0f710b1ccc misc: updated documentation 2025-04-15 15:01:10 +00:00
71c55d5a53 misc: addressed review comments 2025-04-15 22:42:38 +08:00
1d8c513da1 Improve invalidateQueries for useToggleDeleteProjectProtection 2025-04-15 08:07:21 -03:00
ae8a78b883 Fix cron schedule used to test 2025-04-15 07:46:35 -03:00
x
b08b53b77d increase certificate altnames character limit to 4096 2025-04-15 00:40:28 -04:00
86d7fca8fb Add minor improvements to notifyExpiredTokens 2025-04-14 21:52:16 -03:00
cac4f30ca8 Update backend/src/services/service-token/service-token-dal.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-04-14 21:43:19 -03:00
101c056f43 Add project delete protection 2025-04-14 21:41:46 -03:00
80352acc8a Add notification on Service Token expiration 2025-04-14 18:31:06 -03:00
581e4b35f9 rebase 2025-04-11 12:25:26 -07:00
f33a777fae misc: updated form declaration for consistency 2025-04-12 02:04:49 +08:00
8a870131e9 misc: updated missing tx 2025-04-12 02:02:41 +08:00
d97057b43b misc: address metadata type 2025-04-12 01:50:05 +08:00
19b0cd9735 feat: update dynamic secret permissioning 2025-04-12 01:47:33 +08:00
07898414a3 feat: add metadata based permissions for dynamic secret 2025-04-11 00:20:02 +08:00
238 changed files with 4279 additions and 816 deletions

View File

@ -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 }}

View File

@ -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.

View File

@ -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");
});
}
}

View File

@ -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");
});
}
}

View File

@ -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");
});
}
}

View File

@ -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)
});
}

View File

@ -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)
});
}

View File

@ -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");
});
}
}

View File

@ -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");
});
}
}

View File

@ -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>;

View File

@ -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>;

View File

@ -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>;

View File

@ -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>;

View File

@ -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>;

View File

@ -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 };
} }
}); });

View File

@ -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) {

View File

@ -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
});

View File

@ -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
}; };

View File

@ -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) => {

View File

@ -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;
}; };

View File

@ -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 };
}; };

View File

@ -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,7 +136,9 @@ export const dynamicSecretServiceFactory = ({
projectId projectId
}); });
const dynamicSecretCfg = await dynamicSecretDAL.create({ const dynamicSecretCfg = await dynamicSecretDAL.transaction(async (tx) => {
const cfg = await dynamicSecretDAL.create(
{
type: provider.type, type: provider.type,
version: 1, version: 1,
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob, encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
@ -140,7 +147,25 @@ export const dynamicSecretServiceFactory = ({
folderId: folder.id, folderId: folder.id,
name, name,
projectGatewayId: selectedGatewayId 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(
dynamicSecretCfg.id,
{
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) })
.cipherTextBlob,
maxTTL, maxTTL,
defaultTTL, defaultTTL,
name: newName ?? name, name: newName ?? name,
status: null, status: null,
statusDetails: null,
projectGatewayId: selectedGatewayId 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,7 +635,6 @@ 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,
@ -552,15 +644,6 @@ export const dynamicSecretServiceFactory = ({
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)
throw new NotFoundError({ throw new NotFoundError({
@ -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 ({

View File

@ -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 = {

View File

@ -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(

View File

@ -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;

View File

@ -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: [
{ {

View File

@ -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 &&

View File

@ -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({

View File

@ -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()
}), }),

View File

@ -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"
}
}
};

View File

@ -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
};
};

View File

@ -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
});

View File

@ -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
>;

View File

@ -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";

View File

@ -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 {

View File

@ -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 = () => {

View File

@ -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
}; };

View File

@ -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, parameters: payload.parameters,
secretsMapping, secretsMapping,
connection connection
} as TSecretRotationV2WithConnection); } 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,7 +754,8 @@ 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 () =>
secretRotationV2DAL.transaction(async (tx) => {
if (deleteSecrets) { if (deleteSecrets) {
await fnSecretBulkDelete({ await fnSecretBulkDelete({
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
@ -762,10 +777,14 @@ export const secretRotationV2ServiceFactory = ({
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, ...secretRotation,
connection: appConnection connection: appConnection
} as TSecretRotationV2WithConnection); } 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, ...secretRotation,
connection: appConnection connection: appConnection
} as TSecretRotationV2WithConnection); } 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
}); });

View File

@ -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>;
}; };

View File

@ -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
]); ]);

View File

@ -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
}; };
}; };

View File

@ -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}`);
}
};

View File

@ -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.",
@ -1997,12 +2004,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."
} }
} }
}; };

View File

@ -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"
} }

View File

@ -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({

View File

@ -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({

View File

@ -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,
@ -51,7 +52,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedVercelConnectionSchema.options, ...SanitizedVercelConnectionSchema.options,
...SanitizedPostgresConnectionSchema.options, ...SanitizedPostgresConnectionSchema.options,
...SanitizedMsSqlConnectionSchema.options, ...SanitizedMsSqlConnectionSchema.options,
...SanitizedCamundaConnectionSchema.options ...SanitizedCamundaConnectionSchema.options,
...SanitizedAuth0ConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@ -66,7 +68,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
VercelConnectionListItemSchema, VercelConnectionListItemSchema,
PostgresConnectionListItemSchema, PostgresConnectionListItemSchema,
MsSqlConnectionListItemSchema, MsSqlConnectionListItemSchema,
CamundaConnectionListItemSchema CamundaConnectionListItemSchema,
Auth0ConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@ -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 };
}
});
};

View File

@ -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";
@ -28,5 +29,6 @@ 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.Auth0]: registerAuth0ConnectionRouter
}; };

View File

@ -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";
@ -317,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,
@ -343,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
}); });
@ -358,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,

View File

@ -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({

View File

@ -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",

View File

@ -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,

View File

@ -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,

View File

@ -10,7 +10,8 @@ export enum AppConnection {
Vercel = "vercel", Vercel = "vercel",
Postgres = "postgres", Postgres = "postgres",
MsSql = "mssql", MsSql = "mssql",
Camunda = "camunda" Camunda = "camunda",
Auth0 = "auth0"
} }
export enum AWSRegion { export enum AWSRegion {

View File

@ -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,
@ -63,7 +64,8 @@ export const listAppConnectionOptions = () => {
getVercelConnectionListItem(), getVercelConnectionListItem(),
getPostgresConnectionListItem(), getPostgresConnectionListItem(),
getMsSqlConnectionListItem(), getMsSqlConnectionListItem(),
getCamundaConnectionListItem() getCamundaConnectionListItem(),
getAuth0ConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@ -109,6 +111,9 @@ export const decryptAppConnectionCredentials = async ({
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"]; return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
}; };
export const validateAppConnectionCredentials = async (
appConnection: TAppConnectionConfig
): Promise<TAppConnection["credentials"]> => {
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = { const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
@ -120,14 +125,14 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnect
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Camunda]: validateCamundaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator [AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator
}; };
export const validateAppConnectionCredentials = async ( return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
appConnection: TAppConnectionConfig };
): Promise<TAppConnection["credentials"]> => 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 +159,8 @@ 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 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 +203,6 @@ 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.Auth0]: platformManagedCredentialsNotSupported
}; };

View File

@ -12,5 +12,6 @@ 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.Auth0]: "Auth0"
}; };

View File

@ -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";
@ -68,7 +70,8 @@ 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.Auth0]: ValidateAuth0ConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({
@ -442,6 +445,7 @@ 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),
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
}; };
}; };

View File

@ -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,
@ -83,6 +89,7 @@ export type TAppConnection = { id: string } & (
| TPostgresConnection | TPostgresConnection
| TMsSqlConnection | TMsSqlConnection
| TCamundaConnection | TCamundaConnection
| TAuth0Connection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@ -102,6 +109,7 @@ export type TAppConnectionInput = { id: string } & (
| TPostgresConnectionInput | TPostgresConnectionInput
| TMsSqlConnectionInput | TMsSqlConnectionInput
| TCamundaConnectionInput | TCamundaConnectionInput
| TAuth0ConnectionInput
); );
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
@ -124,9 +132,10 @@ export type TAppConnectionConfig =
| TDatabricksConnectionConfig | TDatabricksConnectionConfig
| THumanitecConnectionConfig | THumanitecConnectionConfig
| TTerraformCloudConnectionConfig | TTerraformCloudConnectionConfig
| TVercelConnectionConfig
| TSqlConnectionConfig | TSqlConnectionConfig
| TCamundaConnectionConfig; | TCamundaConnectionConfig
| TVercelConnectionConfig
| TAuth0ConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@ -139,8 +148,9 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidatePostgresConnectionCredentialsSchema | TValidatePostgresConnectionCredentialsSchema
| TValidateMsSqlConnectionCredentialsSchema | TValidateMsSqlConnectionCredentialsSchema
| TValidateCamundaConnectionCredentialsSchema | TValidateCamundaConnectionCredentialsSchema
| TValidateVercelConnectionCredentialsSchema
| TValidateTerraformCloudConnectionCredentialsSchema | TValidateTerraformCloudConnectionCredentialsSchema
| TValidateVercelConnectionCredentialsSchema; | TValidateAuth0ConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;

View File

@ -0,0 +1,3 @@
export enum Auth0ConnectionMethod {
ClientCredentials = "client-credentials"
}

View File

@ -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`
});
}
};

View File

@ -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()
});

View File

@ -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
};
};

View File

@ -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[];
};

View 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";

View File

@ -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",

View File

@ -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()

View File

@ -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"
}); });
} }

View File

@ -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
) { ) {
// bypass server configuration when user is an organization admin - this is to prevent lockout
const userOrgs = await orgDAL.findAllOrgsByUserId(userEnc.userId);
if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Login with email is disabled by administrator." 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,29 +578,41 @@ 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)) {
// bypass server configuration when user is an organization admin - this is to prevent lockout
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Login with Github is disabled by administrator.", message: "Login with Github is disabled by administrator.",
name: "Oauth 2 login" name: "Oauth 2 login"
}); });
} }
}
break; break;
} }
case AuthMethod.GOOGLE: { case AuthMethod.GOOGLE: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GOOGLE)) {
// bypass server configuration when user is an organization admin - this is to prevent lockout
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Login with Google is disabled by administrator.", message: "Login with Google is disabled by administrator.",
name: "Oauth 2 login" name: "Oauth 2 login"
}); });
} }
}
break; break;
} }
case AuthMethod.GITLAB: { case AuthMethod.GITLAB: {
if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) { if (!serverCfg.enabledLoginMethods.includes(LoginMethod.GITLAB)) {
// bypass server configuration when user is an organization admin - this is to prevent lockout
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
if (!userOrgs.some((org) => org.userRole === OrgMembershipRole.Admin)) {
throw new BadRequestError({ throw new BadRequestError({
message: "Login with Gitlab is disabled by administrator.", message: "Login with Gitlab is disabled by administrator.",
name: "Oauth 2 login" name: "Oauth 2 login"
}); });
} }
}
break; break;
} }
default: default:

View File

@ -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) {

View File

@ -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
}); });

View File

@ -349,7 +349,8 @@ export const orgServiceFactory = ({
defaultMembershipRoleSlug, defaultMembershipRoleSlug,
enforceMfa, enforceMfa,
selectedMfaMethod, selectedMfaMethod,
allowSecretSharingOutsideOrganization allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled
} }
}: TUpdateOrgDTO) => { }: TUpdateOrgDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
@ -429,7 +430,8 @@ export const orgServiceFactory = ({
defaultMembershipRole, defaultMembershipRole,
enforceMfa, enforceMfa,
selectedMfaMethod, selectedMfaMethod,
allowSecretSharingOutsideOrganization allowSecretSharingOutsideOrganization,
bypassOrgAuthEnabled
}); });
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
return org; return org;

View File

@ -73,6 +73,7 @@ export type TUpdateOrgDTO = {
enforceMfa: boolean; enforceMfa: boolean;
selectedMfaMethod: MfaMethod; selectedMfaMethod: MfaMethod;
allowSecretSharingOutsideOrganization: boolean; allowSecretSharingOutsideOrganization: boolean;
bypassOrgAuthEnabled: boolean;
}>; }>;
} & TOrgPermission; } & TOrgPermission;

View File

@ -86,6 +86,7 @@ import {
TProjectAccessRequestDTO, TProjectAccessRequestDTO,
TSearchProjectsDTO, TSearchProjectsDTO,
TToggleProjectAutoCapitalizationDTO, TToggleProjectAutoCapitalizationDTO,
TToggleProjectDeleteProtectionDTO,
TUpdateAuditLogsRetentionDTO, TUpdateAuditLogsRetentionDTO,
TUpdateProjectDTO, TUpdateProjectDTO,
TUpdateProjectKmsDTO, TUpdateProjectKmsDTO,
@ -482,6 +483,12 @@ export const projectServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
if (project.hasDeleteProtection) {
throw new ForbiddenRequestError({
message: "Project delete protection is enabled"
});
}
const deletedProject = await projectDAL.transaction(async (tx) => { const deletedProject = await projectDAL.transaction(async (tx) => {
// delete these so that project custom roles can be deleted in cascade effect // delete these so that project custom roles can be deleted in cascade effect
// direct deletion of project without these will cause fk error // direct deletion of project without these will cause fk error
@ -616,6 +623,7 @@ export const projectServiceFactory = ({
description: update.description, description: update.description,
autoCapitalization: update.autoCapitalization, autoCapitalization: update.autoCapitalization,
enforceCapitalization: update.autoCapitalization, enforceCapitalization: update.autoCapitalization,
hasDeleteProtection: update.hasDeleteProtection,
slug: update.slug slug: update.slug
}); });
@ -648,6 +656,29 @@ export const projectServiceFactory = ({
return updatedProject; return updatedProject;
}; };
const toggleDeleteProtection = async ({
projectId,
actor,
actorId,
actorOrgId,
actorAuthMethod,
hasDeleteProtection
}: TToggleProjectDeleteProtectionDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
const updatedProject = await projectDAL.updateById(projectId, { hasDeleteProtection });
return updatedProject;
};
const updateVersionLimit = async ({ const updateVersionLimit = async ({
actor, actor,
actorId, actorId,
@ -1499,6 +1530,7 @@ export const projectServiceFactory = ({
getProjectUpgradeStatus, getProjectUpgradeStatus,
getAProject, getAProject,
toggleAutoCapitalization, toggleAutoCapitalization,
toggleDeleteProtection,
updateName, updateName,
upgradeProject, upgradeProject,
listProjectCas, listProjectCas,

View File

@ -66,6 +66,10 @@ export type TToggleProjectAutoCapitalizationDTO = {
autoCapitalization: boolean; autoCapitalization: boolean;
} & TProjectPermission; } & TProjectPermission;
export type TToggleProjectDeleteProtectionDTO = {
hasDeleteProtection: boolean;
} & TProjectPermission;
export type TUpdateProjectVersionLimitDTO = { export type TUpdateProjectVersionLimitDTO = {
pitVersionLimit: number; pitVersionLimit: number;
workspaceSlug: string; workspaceSlug: string;
@ -86,6 +90,7 @@ export type TUpdateProjectDTO = {
name?: string; name?: string;
description?: string; description?: string;
autoCapitalization?: boolean; autoCapitalization?: boolean;
hasDeleteProtection?: boolean;
slug?: string; slug?: string;
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;

View File

@ -10,6 +10,7 @@ import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal"; import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal"; import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TServiceTokenServiceFactory } from "../service-token/service-token-service";
type TDailyResourceCleanUpQueueServiceFactoryDep = { type TDailyResourceCleanUpQueueServiceFactoryDep = {
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">; auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
@ -21,6 +22,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">; secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">; snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">; secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
serviceTokenService: Pick<TServiceTokenServiceFactory, "notifyExpiringTokens">;
queueService: TQueueServiceFactory; queueService: TQueueServiceFactory;
}; };
@ -36,7 +38,8 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
identityAccessTokenDAL, identityAccessTokenDAL,
secretSharingDAL, secretSharingDAL,
secretVersionV2DAL, secretVersionV2DAL,
identityUniversalAuthClientSecretDAL identityUniversalAuthClientSecretDAL,
serviceTokenService
}: TDailyResourceCleanUpQueueServiceFactoryDep) => { }: TDailyResourceCleanUpQueueServiceFactoryDep) => {
queueService.start(QueueName.DailyResourceCleanUp, async () => { queueService.start(QueueName.DailyResourceCleanUp, async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
@ -50,6 +53,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
await secretVersionDAL.pruneExcessVersions(); await secretVersionDAL.pruneExcessVersions();
await secretVersionV2DAL.pruneExcessVersions(); await secretVersionV2DAL.pruneExcessVersions();
await secretFolderVersionDAL.pruneExcessVersions(); await secretFolderVersionDAL.pruneExcessVersions();
await serviceTokenService.notifyExpiringTokens();
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
}); });

View File

@ -819,10 +819,14 @@ export const secretImportServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( if (
permission.cannot(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
); )
) {
return [];
}
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return []; if (!folder) return [];

View File

@ -28,5 +28,36 @@ export const serviceTokenDALFactory = (db: TDbClient) => {
} }
}; };
return { ...stOrm, findById }; const findExpiringTokens = async (tx?: Knex, batchSize = 500, offset = 0) => {
try {
const batch: { name: string; projectName: string; createdByEmail: string; id: string; projectId: string }[] =
await (tx || db.replicaNode())(TableName.ServiceToken)
.leftJoin<TUsers>(
TableName.Users,
`${TableName.Users}.id`,
db.raw(`${TableName.ServiceToken}."createdBy"::uuid`)
)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.ServiceToken}.projectId`)
.whereRaw(
`${TableName.ServiceToken}."expiresAt" < NOW() + INTERVAL '1 day' AND ${TableName.ServiceToken}."expiryNotificationSent" = false`
)
.whereNotNull(`${TableName.Users}.email`)
.select(
db.ref("id").withSchema(TableName.ServiceToken),
db.ref("name").withSchema(TableName.ServiceToken),
db.ref("projectId").withSchema(TableName.ServiceToken),
db.ref("createdBy").withSchema(TableName.ServiceToken),
db.ref("email").withSchema(TableName.Users).as("createdByEmail"),
db.ref("name").withSchema(TableName.Project).as("projectName")
)
.limit(batchSize)
.offset(offset);
return batch;
} catch (err) {
throw new DatabaseError({ error: err, name: "FindExpiredTokens" });
}
};
return { ...stOrm, findById, findExpiringTokens };
}; };

View File

@ -12,11 +12,13 @@ import {
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue"; import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TServiceTokenDALFactory } from "./service-token-dal"; import { TServiceTokenDALFactory } from "./service-token-dal";
import { import {
@ -33,6 +35,7 @@ type TServiceTokenServiceFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
accessTokenQueue: Pick<TAccessTokenQueueServiceFactory, "updateServiceTokenStatus">; accessTokenQueue: Pick<TAccessTokenQueueServiceFactory, "updateServiceTokenStatus">;
smtpService: Pick<TSmtpService, "sendMail">;
}; };
export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>; export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>;
@ -43,7 +46,8 @@ export const serviceTokenServiceFactory = ({
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
projectDAL, projectDAL,
accessTokenQueue accessTokenQueue,
smtpService
}: TServiceTokenServiceFactoryDep) => { }: TServiceTokenServiceFactoryDep) => {
const createServiceToken = async ({ const createServiceToken = async ({
iv, iv,
@ -185,11 +189,56 @@ export const serviceTokenServiceFactory = ({
return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId }; return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId };
}; };
const notifyExpiringTokens = async () => {
const appCfg = getConfig();
let processedCount = 0;
let hasMoreRecords = true;
let offset = 0;
const batchSize = 500;
while (hasMoreRecords) {
// eslint-disable-next-line no-await-in-loop
const expiringTokens = await serviceTokenDAL.findExpiringTokens(undefined, batchSize, offset);
if (expiringTokens.length === 0) {
hasMoreRecords = false;
break;
}
// eslint-disable-next-line no-await-in-loop
await Promise.all(
expiringTokens.map(async (token) => {
try {
await smtpService.sendMail({
recipients: [token.createdByEmail],
subjectLine: "Service Token Expiry Notice",
template: SmtpTemplates.ServiceTokenExpired,
substitutions: {
tokenName: token.name,
projectName: token.projectName,
url: `${appCfg.SITE_URL}/secret-manager/${token.projectId}/access-management?selectedTab=service-tokens`
}
});
await serviceTokenDAL.update({ id: token.id }, { expiryNotificationSent: true });
} catch (error) {
logger.error(error, `Failed to send expiration notification for token ${token.id}:`);
}
})
);
processedCount += expiringTokens.length;
offset += batchSize;
}
return processedCount;
};
return { return {
createServiceToken, createServiceToken,
deleteServiceToken, deleteServiceToken,
getServiceToken, getServiceToken,
getProjectServiceTokens, getProjectServiceTokens,
fnValidateServiceToken fnValidateServiceToken,
notifyExpiringTokens
}; };
}; };

View File

@ -43,7 +43,8 @@ export enum SmtpTemplates {
SecretRequestCompleted = "secretRequestCompleted.handlebars", SecretRequestCompleted = "secretRequestCompleted.handlebars",
SecretRotationFailed = "secretRotationFailed.handlebars", SecretRotationFailed = "secretRotationFailed.handlebars",
ProjectAccessRequest = "projectAccess.handlebars", ProjectAccessRequest = "projectAccess.handlebars",
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars" OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars",
ServiceTokenExpired = "serviceTokenExpired.handlebars"
} }
export enum SmtpHost { export enum SmtpHost {

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Service Token Expiring Soon</title>
</head>
<body>
<h2>Service Token Expiry Notice</h2>
<p>Your service token <strong>"{{tokenName}}"</strong> will expire within 24 hours.</p>
<p>This token is currently being used on project "{{projectName}}". If this token is still needed for your workflow, please create a new one before it expires.</p>
<a href="{{url}}">Create New Token</a>
{{emailFooter}}
</body>
</html>

View File

@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/auth0/available"
---

View File

@ -0,0 +1,9 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/auth0"
---
<Note>
Check out the configuration docs for [Auth0 Connections](/integrations/app-connections/auth0) to learn how to obtain the
required credentials.
</Note>

View File

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/auth0/{connectionId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/auth0/{connectionId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/auth0/connection-name/{connectionName}"
---

View File

@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/auth0"
---

View File

@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/auth0/{connectionId}"
---
<Note>
Check out the configuration docs for [Auth0 Connections](/integrations/app-connections/auth0) to learn how to obtain the
required credentials.
</Note>

View File

@ -0,0 +1,9 @@
---
title: "Create"
openapi: "POST /api/v2/secret-rotations/auth0-client-secret"
---
<Note>
Check out the configuration docs for [Auth0 Client Secret Rotations](/documentation/platform/secret-rotation/auth0-client-secret) to learn how to obtain the
required parameters.
</Note>

View File

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v2/secret-rotations/auth0-client-secret/{rotationId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v2/secret-rotations/auth0-client-secret/{rotationId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v2/secret-rotations/auth0-client-secret/rotation-name/{rotationName}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get Credentials by ID"
openapi: "GET /api/v2/secret-rotations/auth0-client-secret/{rotationId}/generated-credentials"
---

View File

@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/secret-rotations/auth0-client-secret"
---

View File

@ -0,0 +1,4 @@
---
title: "Rotate Secrets"
openapi: "POST /api/v2/secret-rotations/auth0-client-secret/{rotationId}/rotate-secrets"
---

View File

@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v2/secret-rotations/auth0-client-secret/{rotationId}"
---
<Note>
Check out the configuration docs for [Auth0 Client Secret Rotations](/documentation/platform/secret-rotation/auth0-client-secret) to learn how to obtain the
required parameters.
</Note>

View File

@ -5,6 +5,6 @@ openapi: "POST /api/v2/secret-rotations/mssql-credentials"
<Note> <Note>
Check out the configuration docs for [Microsoft SQL Server Check out the configuration docs for [Microsoft SQL Server
Credentials Rotations](/documentation/platform/secret-rotation/mssql) to learn how to obtain the Credentials Rotations](/documentation/platform/secret-rotation/mssql-credentials) to learn how to obtain the
required parameters. required parameters.
</Note> </Note>

View File

@ -5,6 +5,6 @@ openapi: "PATCH /api/v2/secret-rotations/mssql-credentials/{rotationId}"
<Note> <Note>
Check out the configuration docs for [Microsoft SQL Server Check out the configuration docs for [Microsoft SQL Server
Credentials Rotations](/documentation/platform/secret-rotation/mssql) to learn how to obtain the Credentials Rotations](/documentation/platform/secret-rotation/mssql-credentials) to learn how to obtain the
required parameters. required parameters.
</Note> </Note>

View File

@ -5,6 +5,6 @@ openapi: "POST /api/v2/secret-rotations/postgres-credentials"
<Note> <Note>
Check out the configuration docs for [PostgreSQL Check out the configuration docs for [PostgreSQL
Credentials Rotations](/documentation/platform/secret-rotation/postgres) to learn how to obtain the Credentials Rotations](/documentation/platform/secret-rotation/postgres-credentials) to learn how to obtain the
required parameters. required parameters.
</Note> </Note>

View File

@ -5,6 +5,6 @@ openapi: "PATCH /api/v2/secret-rotations/postgres-credentials/{rotationId}"
<Note> <Note>
Check out the configuration docs for [PostgreSQL Check out the configuration docs for [PostgreSQL
Credentials Rotations](/documentation/platform/secret-rotation/postgres) to learn how to obtain the Credentials Rotations](/documentation/platform/secret-rotation/postgres-credentials) to learn how to obtain the
required parameters. required parameters.
</Note> </Note>

Some files were not shown because too many files have changed in this diff Show More