mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 02:29:09 +00:00
Merge branch 'Infisical:main' into main
This commit is contained in:
Generated
+10
@@ -83,6 +83,7 @@
|
||||
"ioredis": "^5.3.2",
|
||||
"isomorphic-dompurify": "^2.22.0",
|
||||
"jmespath": "^0.16.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsrp": "^0.2.4",
|
||||
"jwks-rsa": "^3.1.0",
|
||||
@@ -143,6 +144,7 @@
|
||||
"@smithy/types": "^4.3.1",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/jmespath": "^0.15.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@types/libsodium-wrappers": "^0.7.13",
|
||||
@@ -13160,6 +13162,13 @@
|
||||
"integrity": "sha512-pegh49FtNsC389Flyo9y8AfkVIZn9MMPE9yJrO9svhq6Fks2MwymULWjZqySuxmctd3ZH4/n7Mr98D+1Qo5vGA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/js-yaml": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -20452,6 +20461,7 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
|
||||
"seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest",
|
||||
"email:dev": "email dev --dir src/services/smtp/emails"
|
||||
"email:dev": "email dev --dir src/services/smtp/emails",
|
||||
"validate-upgrade-path": "tsx ./scripts/validate-upgrade-path-file.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -87,6 +88,7 @@
|
||||
"@smithy/types": "^4.3.1",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/jmespath": "^0.15.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@types/libsodium-wrappers": "^0.7.13",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ioredis": "^5.3.2",
|
||||
"isomorphic-dompurify": "^2.22.0",
|
||||
"jmespath": "^0.16.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsrp": "^0.2.4",
|
||||
"jwks-rsa": "^3.1.0",
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/* eslint-disable no-console */
|
||||
import { readFile } from "fs/promises";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as path from "path";
|
||||
import { z } from "zod";
|
||||
|
||||
import { upgradePathConfigSchema } from "../src/services/upgrade-path/upgrade-path-schemas";
|
||||
|
||||
async function validateUpgradePathConfig(): Promise<void> {
|
||||
try {
|
||||
const yamlPath = path.join(__dirname, "..", "upgrade-path.yaml");
|
||||
const resolvedPath = path.resolve(yamlPath);
|
||||
const expectedBaseDir = path.resolve(__dirname, "..");
|
||||
|
||||
if (!resolvedPath.startsWith(expectedBaseDir)) {
|
||||
throw new Error("Invalid configuration file path");
|
||||
}
|
||||
|
||||
try {
|
||||
await readFile(yamlPath, "utf8");
|
||||
} catch (error) {
|
||||
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
||||
console.log("Warning: No upgrade-path.yaml file found");
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const yamlContent = await readFile(yamlPath, "utf8");
|
||||
|
||||
if (yamlContent.length > 1024 * 1024) {
|
||||
throw new Error("Config file too large (>1MB)");
|
||||
}
|
||||
|
||||
let config: unknown;
|
||||
try {
|
||||
config = yaml.load(yamlContent, {
|
||||
schema: yaml.FAILSAFE_SCHEMA,
|
||||
filename: yamlPath,
|
||||
onWarning: (warning) => {
|
||||
console.log(`YAML Warning: ${warning.message}`);
|
||||
}
|
||||
});
|
||||
} catch (yamlError) {
|
||||
if (yamlError instanceof yaml.YAMLException) {
|
||||
throw new Error(
|
||||
`YAML parsing failed: ${yamlError.message} at line ${yamlError.mark?.line}, column ${yamlError.mark?.column}`
|
||||
);
|
||||
}
|
||||
throw new Error(`YAML parsing failed: ${yamlError instanceof Error ? yamlError.message : "Unknown YAML error"}`);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
console.log("Warning: Empty configuration file");
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof config !== "object" || config === null) {
|
||||
throw new Error("Configuration must be a valid YAML object");
|
||||
}
|
||||
|
||||
const result = upgradePathConfigSchema.safeParse(config);
|
||||
|
||||
if (!result.success) {
|
||||
console.log("Validation failed with the following errors:");
|
||||
result.error.issues.forEach((issue: z.ZodIssue) => {
|
||||
const issuePath = issue.path.length > 0 ? `[${issue.path.join(".")}]` : "";
|
||||
console.log(` - ${issuePath}: ${issue.message}`);
|
||||
});
|
||||
throw new Error("Schema validation failed");
|
||||
}
|
||||
|
||||
const validatedConfig = result.data;
|
||||
const versions = validatedConfig?.versions || {};
|
||||
const versionCount = Object.keys(versions).length;
|
||||
|
||||
if (versionCount === 0) {
|
||||
console.log("Warning: No versions found in the configuration");
|
||||
} else {
|
||||
console.log(`Validated ${versionCount} version configuration(s)`);
|
||||
|
||||
const commonPatterns = [
|
||||
/^v?\d+\.\d+\.\d+$/,
|
||||
/^v?\d+\.\d+\.\d+\.\d+$/,
|
||||
/^infisical\/v?\d+\.\d+\.\d+$/,
|
||||
/^infisical\/v?\d+\.\d+\.\d+-\w+$/
|
||||
];
|
||||
|
||||
for (const versionKey of Object.keys(versions)) {
|
||||
const isCommonPattern = commonPatterns.some((pattern) => pattern.test(versionKey));
|
||||
if (!isCommonPattern) {
|
||||
console.log(`Warning: Version key '${versionKey}' doesn't match common patterns. This may be intentional.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("upgrade-path.yaml format is valid");
|
||||
} catch (error) {
|
||||
console.error(`Validation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
validateUpgradePathConfig().catch((error) => {
|
||||
console.error("Unexpected error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
Vendored
+4
@@ -93,6 +93,7 @@ import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-servi
|
||||
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
|
||||
import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
|
||||
import { TPkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
|
||||
import { TPkiSyncServiceFactory } from "@app/services/pki-sync/pki-sync-service";
|
||||
import { TPkiTemplatesServiceFactory } from "@app/services/pki-templates/pki-templates-service";
|
||||
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
@@ -114,6 +115,7 @@ import { TSlackServiceFactory } from "@app/services/slack/slack-service";
|
||||
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
||||
import { TTotpServiceFactory } from "@app/services/totp/totp-service";
|
||||
import { TUpgradePathService } from "@app/services/upgrade-path/upgrade-path-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
import { TUserServiceFactory } from "@app/services/user/user-service";
|
||||
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
|
||||
@@ -267,6 +269,7 @@ declare module "fastify" {
|
||||
certificateEst: TCertificateEstServiceFactory;
|
||||
pkiCollection: TPkiCollectionServiceFactory;
|
||||
pkiSubscriber: TPkiSubscriberServiceFactory;
|
||||
pkiSync: TPkiSyncServiceFactory;
|
||||
secretScanning: TSecretScanningServiceFactory;
|
||||
license: TLicenseServiceFactory;
|
||||
trustedIp: TTrustedIpServiceFactory;
|
||||
@@ -312,6 +315,7 @@ declare module "fastify" {
|
||||
identityAuthTemplate: TIdentityAuthTemplateServiceFactory;
|
||||
notification: TNotificationServiceFactory;
|
||||
offlineUsageReport: TOfflineUsageReportServiceFactory;
|
||||
upgradePath: TUpgradePathService;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
||||
Vendored
+4
@@ -263,6 +263,9 @@ import {
|
||||
TPkiSubscribers,
|
||||
TPkiSubscribersInsert,
|
||||
TPkiSubscribersUpdate,
|
||||
TPkiSyncs,
|
||||
TPkiSyncsInsert,
|
||||
TPkiSyncsUpdate,
|
||||
TProjectBots,
|
||||
TProjectBotsInsert,
|
||||
TProjectBotsUpdate,
|
||||
@@ -680,6 +683,7 @@ declare module "knex/types/tables" {
|
||||
TPkiSubscribersInsert,
|
||||
TPkiSubscribersUpdate
|
||||
>;
|
||||
[TableName.PkiSync]: KnexOriginal.CompositeTableType<TPkiSyncs, TPkiSyncsInsert, TPkiSyncsUpdate>;
|
||||
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
||||
TUserGroupMembership,
|
||||
TUserGroupMembershipInsert,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.PkiSync))) {
|
||||
await knex.schema.createTable(TableName.PkiSync, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name", 32).notNullable();
|
||||
t.string("description");
|
||||
t.string("destination").notNullable();
|
||||
t.boolean("isAutoSyncEnabled").notNullable().defaultTo(true);
|
||||
t.integer("version").defaultTo(1).notNullable();
|
||||
t.jsonb("destinationConfig").notNullable();
|
||||
t.jsonb("syncOptions").notNullable();
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.uuid("subscriberId");
|
||||
t.foreign("subscriberId").references("id").inTable(TableName.PkiSubscriber).onDelete("SET NULL");
|
||||
t.uuid("connectionId").notNullable();
|
||||
t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
|
||||
t.timestamps(true, true, true);
|
||||
t.string("syncStatus");
|
||||
t.string("lastSyncJobId");
|
||||
t.string("lastSyncMessage");
|
||||
t.datetime("lastSyncedAt");
|
||||
t.string("importStatus");
|
||||
t.string("lastImportJobId");
|
||||
t.string("lastImportMessage");
|
||||
t.datetime("lastImportedAt");
|
||||
t.string("removeStatus");
|
||||
t.string("lastRemoveJobId");
|
||||
t.string("lastRemoveMessage");
|
||||
t.datetime("lastRemovedAt");
|
||||
|
||||
t.unique(["name", "projectId"], { indexName: "pki_syncs_name_project_id_unique" });
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.PkiSync);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.PkiSync);
|
||||
await dropOnUpdateTrigger(knex, TableName.PkiSync);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasAllowedNamespaces = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNamespaces");
|
||||
const hasAllowedNames = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNames");
|
||||
const hasAllowedAudience = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedAudience");
|
||||
|
||||
if (hasAllowedNamespaces || hasAllowedNames || hasAllowedAudience) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
|
||||
if (hasAllowedNames) t.string("allowedNames", 1000).notNullable().alter();
|
||||
if (hasAllowedNamespaces) t.string("allowedNamespaces", 1000).notNullable().alter();
|
||||
if (hasAllowedAudience) t.string("allowedAudience", 1000).notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasAllowedNamespaces = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNamespaces");
|
||||
const hasAllowedNames = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNames");
|
||||
const hasAllowedAudience = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedAudience");
|
||||
|
||||
if (hasAllowedNamespaces || hasAllowedNames || hasAllowedAudience) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
|
||||
if (hasAllowedNames) t.string("allowedNames", 255).notNullable().alter();
|
||||
if (hasAllowedNamespaces) t.string("allowedNamespaces", 255).notNullable().alter();
|
||||
if (hasAllowedAudience) t.string("allowedAudience", 255).notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretApprovalRequestSecretV2)) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
|
||||
t.boolean("skipMultilineEncoding").alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretApprovalRequestSecretV2)) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
|
||||
t.boolean("skipMultilineEncoding").defaultTo(false).alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,7 @@ export * from "./pki-alerts";
|
||||
export * from "./pki-collection-items";
|
||||
export * from "./pki-collections";
|
||||
export * from "./pki-subscribers";
|
||||
export * from "./pki-syncs";
|
||||
export * from "./project-bots";
|
||||
export * from "./project-environments";
|
||||
export * from "./project-gateways";
|
||||
|
||||
@@ -156,6 +156,7 @@ export enum TableName {
|
||||
ProjectSlackConfigs = "project_slack_configs",
|
||||
AppConnection = "app_connections",
|
||||
SecretSync = "secret_syncs",
|
||||
PkiSync = "pki_syncs",
|
||||
KmipClient = "kmip_clients",
|
||||
KmipOrgConfig = "kmip_org_configs",
|
||||
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const PkiSyncsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
destination: z.string(),
|
||||
isAutoSyncEnabled: z.boolean().default(true),
|
||||
version: z.number().default(1),
|
||||
destinationConfig: z.unknown(),
|
||||
syncOptions: z.unknown(),
|
||||
projectId: z.string(),
|
||||
subscriberId: z.string().uuid().nullable().optional(),
|
||||
connectionId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
syncStatus: z.string().nullable().optional(),
|
||||
lastSyncJobId: z.string().nullable().optional(),
|
||||
lastSyncMessage: z.string().nullable().optional(),
|
||||
lastSyncedAt: z.date().nullable().optional(),
|
||||
importStatus: z.string().nullable().optional(),
|
||||
lastImportJobId: z.string().nullable().optional(),
|
||||
lastImportMessage: z.string().nullable().optional(),
|
||||
lastImportedAt: z.date().nullable().optional(),
|
||||
removeStatus: z.string().nullable().optional(),
|
||||
lastRemoveJobId: z.string().nullable().optional(),
|
||||
lastRemoveMessage: z.string().nullable().optional(),
|
||||
lastRemovedAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TPkiSyncs = z.infer<typeof PkiSyncsSchema>;
|
||||
export type TPkiSyncsInsert = Omit<z.input<typeof PkiSyncsSchema>, TImmutableDBKeys>;
|
||||
export type TPkiSyncsUpdate = Partial<Omit<z.input<typeof PkiSyncsSchema>, TImmutableDBKeys>>;
|
||||
@@ -17,7 +17,7 @@ export const SecretApprovalRequestsSecretsV2Schema = z.object({
|
||||
encryptedComment: zodBuffer.nullable().optional(),
|
||||
reminderNote: z.string().nullable().optional(),
|
||||
reminderRepeatDays: z.number().nullable().optional(),
|
||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||
skipMultilineEncoding: z.boolean().nullable().optional(),
|
||||
metadata: z.unknown().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
backfillPermissionV1SchemaToV2Schema,
|
||||
ProjectPermissionV1Schema
|
||||
@@ -50,6 +51,10 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const stringifiedPermissions = JSON.stringify(
|
||||
packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))
|
||||
);
|
||||
|
||||
const role = await server.services.projectRole.createRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
@@ -61,7 +66,23 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
},
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -106,6 +127,10 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const stringifiedPermissions = req.body.permissions
|
||||
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
|
||||
: undefined;
|
||||
|
||||
const role = await server.services.projectRole.updateRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
@@ -114,11 +139,26 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
roleId: req.params.roleId,
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: req.body.permissions
|
||||
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
|
||||
: undefined
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -155,6 +195,21 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
actor: req.permission.type,
|
||||
roleId: req.params.roleId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: role.slug,
|
||||
name: role.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@@ -42,6 +43,22 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.CREATE_ORG_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: JSON.stringify(req.body.permissions)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -116,6 +133,22 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_ORG_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -146,6 +179,16 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.DELETE_ORG_ROLE,
|
||||
metadata: { roleId: role.id, slug: role.slug, name: role.name }
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||
import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs";
|
||||
@@ -52,6 +53,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions));
|
||||
|
||||
const role = await server.services.projectRole.createRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
@@ -63,9 +66,26 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -112,6 +132,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined;
|
||||
const role = await server.services.projectRole.updateRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
@@ -120,9 +141,26 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
roleId: req.params.roleId,
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -161,6 +199,21 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
actor: req.permission.type,
|
||||
roleId: req.params.roleId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: role.slug,
|
||||
name: role.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { RelaysSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -89,14 +90,59 @@ export const registerRelayRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
throw new BadRequestError({
|
||||
message: "Org relay registration is not yet supported"
|
||||
});
|
||||
|
||||
return server.services.relay.registerRelay({
|
||||
...req.body,
|
||||
identityId: req.permission.id,
|
||||
orgId: req.permission.orgId
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
schema: {
|
||||
response: {
|
||||
200: RelaysSchema.array()
|
||||
}
|
||||
},
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
return server.services.relay.getRelays({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: RelaysSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
return server.services.relay.deleteRelay({
|
||||
id: req.params.id,
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -320,10 +320,20 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
.array(),
|
||||
secretPath: z.string(),
|
||||
commits: secretRawSchema
|
||||
.omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true })
|
||||
.omit({
|
||||
_id: true,
|
||||
environment: true,
|
||||
workspace: true,
|
||||
type: true,
|
||||
version: true,
|
||||
secretValue: true,
|
||||
secretComment: true
|
||||
})
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
secretValue: z.string().optional(),
|
||||
secretComment: z.string().optional(),
|
||||
skipMultilineEncoding: z.boolean().nullish(),
|
||||
isRotatedSecret: z.boolean().optional(),
|
||||
op: z.string(),
|
||||
tags: SanitizedTagSchema.array().optional(),
|
||||
@@ -348,7 +358,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
secretValueHidden: z.boolean(),
|
||||
secretComment: z.string().optional(),
|
||||
tags: SanitizedTagSchema.array().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.nullish()
|
||||
secretMetadata: ResourceMetadataSchema.nullish(),
|
||||
skipMultilineEncoding: z.boolean().nullish()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretName: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.secretName)
|
||||
}),
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.workspaceId),
|
||||
projectId: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.projectId),
|
||||
environment: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.environment),
|
||||
secretPath: z
|
||||
.string()
|
||||
@@ -54,7 +54,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { secretName } = req.params;
|
||||
const { secretPath, environment, workspaceId: projectId } = req.query;
|
||||
const { secretPath, environment, projectId } = req.query;
|
||||
|
||||
return server.services.secret.getSecretAccessList({
|
||||
actorId: req.permission.id,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||
import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs";
|
||||
@@ -52,6 +53,8 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions));
|
||||
|
||||
const role = await server.services.projectRole.createRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
@@ -63,9 +66,26 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
},
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -112,6 +132,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined;
|
||||
const role = await server.services.projectRole.updateRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
@@ -120,9 +141,26 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
roleId: req.params.roleId,
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -161,6 +199,21 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
actor: req.permission.type,
|
||||
roleId: req.params.roleId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: role.slug,
|
||||
name: role.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rota
|
||||
import { registerOktaClientSecretRotationRouter } from "./okta-client-secret-rotation-router";
|
||||
import { registerOracleDBCredentialsRotationRouter } from "./oracledb-credentials-rotation-router";
|
||||
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
||||
import { registerRedisCredentialsRotationRouter } from "./redis-credentials-rotation-router";
|
||||
|
||||
export * from "./secret-rotation-v2-router";
|
||||
|
||||
@@ -24,5 +25,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
||||
[SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter,
|
||||
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
|
||||
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
|
||||
[SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter
|
||||
[SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter,
|
||||
[SecretRotation.RedisCredentials]: registerRedisCredentialsRotationRouter
|
||||
};
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
CreateRedisCredentialsRotationSchema,
|
||||
RedisCredentialsRotationGeneratedCredentialsSchema,
|
||||
RedisCredentialsRotationSchema,
|
||||
UpdateRedisCredentialsRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/redis-credentials";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||
|
||||
export const registerRedisCredentialsRotationRouter = async (server: FastifyZodProvider) =>
|
||||
registerSecretRotationEndpoints({
|
||||
type: SecretRotation.RedisCredentials,
|
||||
server,
|
||||
responseSchema: RedisCredentialsRotationSchema,
|
||||
createSchema: CreateRedisCredentialsRotationSchema,
|
||||
updateSchema: UpdateRedisCredentialsRotationSchema,
|
||||
generatedCredentialsSchema: RedisCredentialsRotationGeneratedCredentialsSchema
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-
|
||||
import { OktaClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
|
||||
import { OracleDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
|
||||
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
import { RedisCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/redis-credentials";
|
||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||
import { ApiDocsTags, SecretRotations } from "@app/lib/api-docs";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
@@ -25,7 +26,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
||||
AzureClientSecretRotationListItemSchema,
|
||||
AwsIamUserSecretRotationListItemSchema,
|
||||
LdapPasswordRotationListItemSchema,
|
||||
OktaClientSecretRotationListItemSchema
|
||||
OktaClientSecretRotationListItemSchema,
|
||||
RedisCredentialsRotationListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -35,7 +35,7 @@ import { ApprovalStatus, TAccessApprovalRequestServiceFactory } from "./access-a
|
||||
|
||||
type TSecretApprovalRequestServiceFactoryDep = {
|
||||
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "create" | "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "invalidateProjectPermissionCache">;
|
||||
accessApprovalPolicyApproverDAL: Pick<TAccessApprovalPolicyApproverDALFactory, "find">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<
|
||||
@@ -758,6 +758,8 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
{ privilegeId: privilegeIdToSet, status: ApprovalStatus.APPROVED },
|
||||
tx
|
||||
);
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(accessApprovalRequest.projectId, tx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -407,6 +407,14 @@ export enum EventType {
|
||||
SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets",
|
||||
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
|
||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
||||
GET_PKI_SYNCS = "get-pki-syncs",
|
||||
GET_PKI_SYNC = "get-pki-sync",
|
||||
CREATE_PKI_SYNC = "create-pki-sync",
|
||||
UPDATE_PKI_SYNC = "update-pki-sync",
|
||||
DELETE_PKI_SYNC = "delete-pki-sync",
|
||||
PKI_SYNC_SYNC_CERTIFICATES = "pki-sync-sync-certificates",
|
||||
PKI_SYNC_IMPORT_CERTIFICATES = "pki-sync-import-certificates",
|
||||
PKI_SYNC_REMOVE_CERTIFICATES = "pki-sync-remove-certificates",
|
||||
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
|
||||
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user",
|
||||
CREATE_KMIP_CLIENT = "create-kmip-client",
|
||||
@@ -478,9 +486,21 @@ export enum EventType {
|
||||
UPDATE_PROJECT = "update-project",
|
||||
DELETE_PROJECT = "delete-project",
|
||||
|
||||
CREATE_PROJECT_ROLE = "create-project-role",
|
||||
UPDATE_PROJECT_ROLE = "update-project-role",
|
||||
DELETE_PROJECT_ROLE = "delete-project-role",
|
||||
|
||||
CREATE_ORG_ROLE = "create-org-role",
|
||||
UPDATE_ORG_ROLE = "update-org-role",
|
||||
DELETE_ORG_ROLE = "delete-org-role",
|
||||
|
||||
CREATE_SECRET_REMINDER = "create-secret-reminder",
|
||||
GET_SECRET_REMINDER = "get-secret-reminder",
|
||||
DELETE_SECRET_REMINDER = "delete-secret-reminder"
|
||||
DELETE_SECRET_REMINDER = "delete-secret-reminder",
|
||||
|
||||
DASHBOARD_LIST_SECRETS = "dashboard-list-secrets",
|
||||
DASHBOARD_GET_SECRET_VALUE = "dashboard-get-secret-value",
|
||||
DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value"
|
||||
}
|
||||
|
||||
export const filterableSecretEvents: EventType[] = [
|
||||
@@ -591,6 +611,7 @@ interface CreateSecretEvent {
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
secretTags?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -605,6 +626,7 @@ interface CreateSecretBatchEvent {
|
||||
secretPath?: string;
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
secretTags?: string[];
|
||||
}>;
|
||||
};
|
||||
}
|
||||
@@ -618,6 +640,7 @@ interface UpdateSecretEvent {
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
secretTags?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -632,6 +655,7 @@ interface UpdateSecretBatchEvent {
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
secretPath?: string;
|
||||
secretTags?: string[];
|
||||
}>;
|
||||
};
|
||||
}
|
||||
@@ -2943,6 +2967,77 @@ interface SecretSyncRemoveSecretsEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetPkiSyncsEvent {
|
||||
type: EventType.GET_PKI_SYNCS;
|
||||
metadata: {
|
||||
projectId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetPkiSyncEvent {
|
||||
type: EventType.GET_PKI_SYNC;
|
||||
metadata: {
|
||||
destination: string;
|
||||
syncId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreatePkiSyncEvent {
|
||||
type: EventType.CREATE_PKI_SYNC;
|
||||
metadata: {
|
||||
pkiSyncId: string;
|
||||
name: string;
|
||||
destination: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdatePkiSyncEvent {
|
||||
type: EventType.UPDATE_PKI_SYNC;
|
||||
metadata: {
|
||||
pkiSyncId: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeletePkiSyncEvent {
|
||||
type: EventType.DELETE_PKI_SYNC;
|
||||
metadata: {
|
||||
pkiSyncId: string;
|
||||
name: string;
|
||||
destination: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PkiSyncSyncCertificatesEvent {
|
||||
type: EventType.PKI_SYNC_SYNC_CERTIFICATES;
|
||||
metadata: {
|
||||
syncId: string;
|
||||
syncMessage: string | null;
|
||||
jobId: string;
|
||||
jobRanAt: Date;
|
||||
};
|
||||
}
|
||||
|
||||
interface PkiSyncImportCertificatesEvent {
|
||||
type: EventType.PKI_SYNC_IMPORT_CERTIFICATES;
|
||||
metadata: {
|
||||
syncId: string;
|
||||
importMessage: string | null;
|
||||
jobId: string;
|
||||
jobRanAt: Date;
|
||||
};
|
||||
}
|
||||
|
||||
interface PkiSyncRemoveCertificatesEvent {
|
||||
type: EventType.PKI_SYNC_REMOVE_CERTIFICATES;
|
||||
metadata: {
|
||||
syncId: string;
|
||||
removeMessage: string | null;
|
||||
jobId: string;
|
||||
jobRanAt: Date;
|
||||
};
|
||||
}
|
||||
|
||||
interface OidcGroupMembershipMappingAssignUserEvent {
|
||||
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER;
|
||||
metadata: {
|
||||
@@ -3502,6 +3597,96 @@ interface ProjectDeleteEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface DashboardListSecretsEvent {
|
||||
type: EventType.DASHBOARD_LIST_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
numberOfSecrets: number;
|
||||
secretIds: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DashboardGetSecretValueEvent {
|
||||
type: EventType.DASHBOARD_GET_SECRET_VALUE;
|
||||
metadata: {
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DashboardGetSecretVersionValueEvent {
|
||||
type: EventType.DASHBOARD_GET_SECRET_VERSION_VALUE;
|
||||
metadata: {
|
||||
secretId: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectRoleCreateEvent {
|
||||
type: EventType.CREATE_PROJECT_ROLE;
|
||||
metadata: {
|
||||
roleId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
permissions: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectRoleUpdateEvent {
|
||||
type: EventType.UPDATE_PROJECT_ROLE;
|
||||
metadata: {
|
||||
roleId: string;
|
||||
slug?: string;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
permissions?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectRoleDeleteEvent {
|
||||
type: EventType.DELETE_PROJECT_ROLE;
|
||||
metadata: {
|
||||
roleId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface OrgRoleCreateEvent {
|
||||
type: EventType.CREATE_ORG_ROLE;
|
||||
metadata: {
|
||||
roleId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
permissions: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface OrgRoleUpdateEvent {
|
||||
type: EventType.UPDATE_ORG_ROLE;
|
||||
metadata: {
|
||||
roleId: string;
|
||||
slug?: string;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
permissions?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface OrgRoleDeleteEvent {
|
||||
type: EventType.DELETE_ORG_ROLE;
|
||||
metadata: {
|
||||
roleId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -3753,6 +3938,14 @@ export type Event =
|
||||
| SecretSyncSyncSecretsEvent
|
||||
| SecretSyncImportSecretsEvent
|
||||
| SecretSyncRemoveSecretsEvent
|
||||
| GetPkiSyncsEvent
|
||||
| GetPkiSyncEvent
|
||||
| CreatePkiSyncEvent
|
||||
| UpdatePkiSyncEvent
|
||||
| DeletePkiSyncEvent
|
||||
| PkiSyncSyncCertificatesEvent
|
||||
| PkiSyncImportCertificatesEvent
|
||||
| PkiSyncRemoveCertificatesEvent
|
||||
| OidcGroupMembershipMappingAssignUserEvent
|
||||
| OidcGroupMembershipMappingRemoveUserEvent
|
||||
| CreateKmipClientEvent
|
||||
@@ -3818,4 +4011,13 @@ export type Event =
|
||||
| ProjectDeleteEvent
|
||||
| SecretReminderCreateEvent
|
||||
| SecretReminderGetEvent
|
||||
| SecretReminderDeleteEvent;
|
||||
| SecretReminderDeleteEvent
|
||||
| DashboardListSecretsEvent
|
||||
| DashboardGetSecretValueEvent
|
||||
| DashboardGetSecretVersionValueEvent
|
||||
| ProjectRoleCreateEvent
|
||||
| ProjectRoleUpdateEvent
|
||||
| ProjectRoleDeleteEvent
|
||||
| OrgRoleCreateEvent
|
||||
| OrgRoleUpdateEvent
|
||||
| OrgRoleDeleteEvent;
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
import handlebars from "handlebars";
|
||||
import knex from "knex";
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretAzureSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
const DEFAULT_PASSWORD_REQUIREMENTS = {
|
||||
length: 48,
|
||||
required: {
|
||||
lowercase: 1,
|
||||
uppercase: 1,
|
||||
digits: 1,
|
||||
symbols: 0
|
||||
},
|
||||
allowedSymbols: "-_.~!*"
|
||||
};
|
||||
|
||||
const generatePassword = (requirements?: PasswordRequirements) => {
|
||||
const finalReqs = requirements || DEFAULT_PASSWORD_REQUIREMENTS;
|
||||
|
||||
try {
|
||||
const { length, required, allowedSymbols } = finalReqs;
|
||||
|
||||
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[crypto.randomInt(chars.lowercase.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.uppercase > 0) {
|
||||
parts.push(
|
||||
...Array(required.uppercase)
|
||||
.fill(0)
|
||||
.map(() => chars.uppercase[crypto.randomInt(chars.uppercase.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.digits > 0) {
|
||||
parts.push(
|
||||
...Array(required.digits)
|
||||
.fill(0)
|
||||
.map(() => chars.digits[crypto.randomInt(chars.digits.length)])
|
||||
);
|
||||
}
|
||||
|
||||
if (required.symbols > 0) {
|
||||
parts.push(
|
||||
...Array(required.symbols)
|
||||
.fill(0)
|
||||
.map(() => chars.symbols[crypto.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[crypto.randomInt(allowedChars.length)])
|
||||
);
|
||||
|
||||
// shuffle the array to mix up the characters
|
||||
for (let i = parts.length - 1; i > 0; i -= 1) {
|
||||
const j = crypto.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 generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(32);
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
return compileUsernameTemplate({
|
||||
usernameTemplate,
|
||||
randomUsername,
|
||||
identity
|
||||
});
|
||||
};
|
||||
|
||||
type TAzureSqlDatabaseProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||
};
|
||||
|
||||
export const AzureSqlDatabaseProvider = ({
|
||||
gatewayService,
|
||||
gatewayV2Service
|
||||
}: TAzureSqlDatabaseProviderDTO): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretAzureSqlDBSchema.parseAsync(inputs);
|
||||
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
|
||||
validateHandlebarTemplate("Azure SQL master creation", providerInputs.masterCreationStatement, {
|
||||
allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val)
|
||||
});
|
||||
validateHandlebarTemplate("Azure SQL creation", providerInputs.creationStatement, {
|
||||
allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val)
|
||||
});
|
||||
if (providerInputs.renewStatement) {
|
||||
validateHandlebarTemplate("Azure SQL renew", providerInputs.renewStatement, {
|
||||
allowedExpressions: (val) => ["username", "expiration", "database"].includes(val)
|
||||
});
|
||||
}
|
||||
validateHandlebarTemplate("Azure SQL revoke", providerInputs.revocationStatement, {
|
||||
allowedExpressions: (val) => ["username", "database"].includes(val)
|
||||
});
|
||||
|
||||
return { ...providerInputs, hostIp };
|
||||
};
|
||||
|
||||
const $getClient = async (
|
||||
providerInputs: z.infer<typeof DynamicSecretAzureSqlDBSchema> & { hostIp: string; originalHost: string },
|
||||
targetDatabase?: string
|
||||
) => {
|
||||
const ssl = providerInputs.ca
|
||||
? { rejectUnauthorized: false, ca: providerInputs.ca, servername: providerInputs.host }
|
||||
: undefined;
|
||||
|
||||
/*
|
||||
We route through the gateway by setting connection.host = "localhost".
|
||||
Azure SQL identifies the logical server from the TDS login name when the host
|
||||
isn't the Azure FQDN. Therefore, when using the gateway, ensure username is
|
||||
"user@<azure-server-name>" so Azure opens the correct logical server.
|
||||
Direct connections to the Azure FQDN usually don't require this suffix.
|
||||
*/
|
||||
const isAzureSql = new RE2(/\.database\.windows\.net$/i).test(providerInputs.originalHost);
|
||||
const azureServerLabel =
|
||||
isAzureSql && providerInputs.gatewayId ? providerInputs.originalHost?.split(".")[0] : undefined;
|
||||
const effectiveUser =
|
||||
isAzureSql && !providerInputs.username.includes("@") && azureServerLabel
|
||||
? `${providerInputs.username}@${azureServerLabel}`
|
||||
: providerInputs.username;
|
||||
|
||||
const db = knex({
|
||||
client: SqlProviders.MsSQL,
|
||||
connection: {
|
||||
database: targetDatabase || providerInputs.database,
|
||||
port: providerInputs.port,
|
||||
host: providerInputs.host,
|
||||
user: effectiveUser,
|
||||
password: providerInputs.password,
|
||||
ssl,
|
||||
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
|
||||
// https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66
|
||||
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
|
||||
options: {
|
||||
...(providerInputs.sslEnabled !== undefined ? { encrypt: providerInputs.sslEnabled } : {}),
|
||||
trustServerCertificate: !providerInputs.ca,
|
||||
cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {}
|
||||
}
|
||||
},
|
||||
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
pool: { min: 0, max: 7 }
|
||||
});
|
||||
return db;
|
||||
};
|
||||
|
||||
const gatewayProxyWrapper = async (
|
||||
providerInputs: z.infer<typeof DynamicSecretAzureSqlDBSchema>,
|
||||
gatewayCallback: (host: string, port: number) => Promise<void>
|
||||
) => {
|
||||
const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
||||
gatewayId: providerInputs.gatewayId as string,
|
||||
targetHost: providerInputs.host,
|
||||
targetPort: providerInputs.port
|
||||
});
|
||||
|
||||
if (gatewayV2ConnectionDetails) {
|
||||
return withGatewayV2Proxy(
|
||||
async (port) => {
|
||||
await gatewayCallback("localhost", port);
|
||||
},
|
||||
{
|
||||
relayHost: gatewayV2ConnectionDetails.relayHost,
|
||||
gateway: gatewayV2ConnectionDetails.gateway,
|
||||
relay: gatewayV2ConnectionDetails.relay,
|
||||
protocol: GatewayProxyProtocol.Tcp
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
|
||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||
await withGatewayProxy(
|
||||
async (port) => {
|
||||
await gatewayCallback("localhost", port);
|
||||
},
|
||||
{
|
||||
protocol: GatewayProxyProtocol.Tcp,
|
||||
targetHost: providerInputs.host,
|
||||
targetPort: providerInputs.port,
|
||||
relayHost,
|
||||
relayPort: Number(relayPort),
|
||||
identityId: relayDetails.identityId,
|
||||
orgId: relayDetails.orgId,
|
||||
tlsOptions: {
|
||||
ca: relayDetails.certChain,
|
||||
cert: relayDetails.certificate,
|
||||
key: relayDetails.privateKey.toString()
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
let isConnected = false;
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({
|
||||
...providerInputs,
|
||||
port,
|
||||
host,
|
||||
hostIp: providerInputs.hostIp,
|
||||
originalHost: providerInputs.host
|
||||
});
|
||||
|
||||
try {
|
||||
isConnected = await db.raw("SELECT 1").then(() => true);
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [providerInputs.username]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (data: {
|
||||
inputs: unknown;
|
||||
expireAt: number;
|
||||
usernameTemplate?: string | null;
|
||||
identity?: { name: string };
|
||||
}) => {
|
||||
const { inputs, expireAt, usernameTemplate, identity } = data;
|
||||
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const { database, masterDatabase } = providerInputs;
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const password = generatePassword(providerInputs.passwordRequirements);
|
||||
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const masterDb = await $getClient(
|
||||
{
|
||||
...providerInputs,
|
||||
port,
|
||||
host,
|
||||
originalHost: providerInputs.host
|
||||
},
|
||||
masterDatabase
|
||||
);
|
||||
|
||||
try {
|
||||
const masterCreationStatement = handlebars.compile(providerInputs.masterCreationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
|
||||
const masterQueries = masterCreationStatement.toString().split(";").filter(Boolean);
|
||||
await masterDb.transaction(async (tx) => {
|
||||
for (const query of masterQueries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create login in master database: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await masterDb.destroy();
|
||||
}
|
||||
|
||||
const targetDb = await $getClient({
|
||||
...providerInputs,
|
||||
port,
|
||||
host,
|
||||
originalHost: providerInputs.host
|
||||
});
|
||||
|
||||
try {
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
await targetDb.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, password, database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create user in target database: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await targetDb.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const username = entityId;
|
||||
const { database, masterDatabase } = providerInputs;
|
||||
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
|
||||
const userDropQueries = queries.filter((query) => query.toLowerCase().includes("drop user"));
|
||||
const loginDropQueries = queries.filter((query) => query.toLowerCase().includes("drop login"));
|
||||
|
||||
if (userDropQueries.length > 0) {
|
||||
const targetDb = await $getClient({
|
||||
...providerInputs,
|
||||
port,
|
||||
host,
|
||||
originalHost: providerInputs.host
|
||||
});
|
||||
|
||||
try {
|
||||
await targetDb.transaction(async (tx) => {
|
||||
for (const query of userDropQueries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query.trim());
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to drop user from target database: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await targetDb.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
if (loginDropQueries.length > 0) {
|
||||
const masterDb = await $getClient(
|
||||
{
|
||||
...providerInputs,
|
||||
port,
|
||||
host,
|
||||
originalHost: providerInputs.host
|
||||
},
|
||||
masterDatabase
|
||||
);
|
||||
|
||||
try {
|
||||
await masterDb.transaction(async (tx) => {
|
||||
for (const query of loginDropQueries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query.trim());
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to drop login from master database: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await masterDb.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
const otherQueries = queries.filter(
|
||||
(query) => !query.toLowerCase().includes("drop user") && !query.toLowerCase().includes("drop login")
|
||||
);
|
||||
|
||||
if (otherQueries.length > 0) {
|
||||
const targetDb = await $getClient({
|
||||
...providerInputs,
|
||||
port,
|
||||
host,
|
||||
originalHost: providerInputs.host
|
||||
});
|
||||
|
||||
try {
|
||||
await targetDb.transaction(async (tx) => {
|
||||
for (const query of otherQueries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query.trim());
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [username, database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to execute revocation statement: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await targetDb.destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (providerInputs.gatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
if (!providerInputs.renewStatement) return { entityId };
|
||||
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({
|
||||
...providerInputs,
|
||||
port,
|
||||
host,
|
||||
originalHost: providerInputs.host
|
||||
});
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { database } = providerInputs;
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||
username: entityId,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
try {
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: [database]
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
};
|
||||
if (providerInputs.gatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
|
||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||
import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
import { AzureSqlDatabaseProvider } from "./azure-sql-database";
|
||||
import { CassandraProvider } from "./cassandra";
|
||||
import { CouchbaseProvider } from "./couchbase";
|
||||
import { ElasticSearchProvider } from "./elastic-search";
|
||||
@@ -42,6 +43,7 @@ export const buildDynamicSecretProviders = ({
|
||||
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
|
||||
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
|
||||
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
|
||||
[DynamicSecretProviders.AzureSqlDatabase]: AzureSqlDatabaseProvider({ gatewayService, gatewayV2Service }),
|
||||
[DynamicSecretProviders.Ldap]: LdapProvider(),
|
||||
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
|
||||
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
|
||||
|
||||
@@ -327,6 +327,44 @@ export const AzureEntraIDSchema = z.object({
|
||||
clientSecret: z.string().trim().min(1)
|
||||
});
|
||||
|
||||
export const DynamicSecretAzureSqlDBSchema = z.object({
|
||||
host: z.string().trim().toLowerCase(),
|
||||
port: z.number(),
|
||||
database: z.string().trim(),
|
||||
masterDatabase: z.string().trim().optional().default("master"),
|
||||
username: z.string().trim(),
|
||||
password: z.string().trim(),
|
||||
passwordRequirements: z
|
||||
.object({
|
||||
length: z.number().min(1).max(250),
|
||||
required: z
|
||||
.object({
|
||||
lowercase: z.number().min(0),
|
||||
uppercase: z.number().min(0),
|
||||
digits: z.number().min(0),
|
||||
symbols: z.number().min(0)
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
|
||||
return total <= 250;
|
||||
}, "Sum of required characters cannot exceed 250"),
|
||||
allowedSymbols: z.string().optional()
|
||||
})
|
||||
.refine((data) => {
|
||||
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
|
||||
return total <= data.length;
|
||||
}, "Sum of required characters cannot exceed the total length")
|
||||
.optional()
|
||||
.describe("Password generation requirements"),
|
||||
masterCreationStatement: z.string().trim(),
|
||||
creationStatement: z.string().trim(),
|
||||
revocationStatement: z.string().trim(),
|
||||
renewStatement: z.string().trim().optional(),
|
||||
ca: z.string().optional(),
|
||||
sslEnabled: z.boolean().optional(),
|
||||
gatewayId: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export const LdapSchema = z.union([
|
||||
z.object({
|
||||
url: z.string().trim().min(1),
|
||||
@@ -610,6 +648,7 @@ export enum DynamicSecretProviders {
|
||||
MongoDB = "mongo-db",
|
||||
RabbitMq = "rabbit-mq",
|
||||
AzureEntraID = "azure-entra-id",
|
||||
AzureSqlDatabase = "azure-sql-database",
|
||||
Ldap = "ldap",
|
||||
SapHana = "sap-hana",
|
||||
Snowflake = "snowflake",
|
||||
@@ -635,6 +674,7 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.AzureSqlDatabase), inputs: DynamicSecretAzureSqlDBSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }),
|
||||
|
||||
@@ -395,7 +395,8 @@ export const gatewayV2ServiceFactory = ({
|
||||
relayId: gateway.relayId,
|
||||
orgId: gateway.orgId,
|
||||
orgName: gateway.orgName,
|
||||
gatewayId
|
||||
gatewayId,
|
||||
gatewayName: gateway.name
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -508,7 +509,8 @@ export const gatewayV2ServiceFactory = ({
|
||||
const relayCredentials = await relayService.getCredentialsForGateway({
|
||||
relayName,
|
||||
orgId,
|
||||
gatewayId: gateway.id
|
||||
gatewayId: gateway.id,
|
||||
gatewayName: gateway.name
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -44,7 +44,10 @@ type TGroupServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||
permissionService: Pick<
|
||||
TPermissionServiceFactory,
|
||||
"getOrgPermission" | "getOrgPermissionByRole" | "invalidateProjectPermissionCache"
|
||||
>;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
|
||||
};
|
||||
@@ -225,6 +228,15 @@ export const groupServiceFactory = ({
|
||||
return updated;
|
||||
});
|
||||
|
||||
if (role) {
|
||||
const groupProjects = await groupProjectDAL.find({ groupId: group.id });
|
||||
await Promise.allSettled([
|
||||
...groupProjects.map((groupProject) =>
|
||||
permissionService.invalidateProjectPermissionCache(groupProject.projectId)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
return updatedGroup;
|
||||
};
|
||||
|
||||
@@ -247,11 +259,17 @@ export const groupServiceFactory = ({
|
||||
message: "Failed to delete group due to plan restriction. Upgrade plan to delete group."
|
||||
});
|
||||
|
||||
const groupProjects = await groupProjectDAL.find({ groupId: id });
|
||||
|
||||
const [group] = await groupDAL.delete({
|
||||
id,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
await Promise.allSettled([
|
||||
...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId))
|
||||
]);
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
@@ -398,6 +416,11 @@ export const groupServiceFactory = ({
|
||||
projectBotDAL
|
||||
});
|
||||
|
||||
const groupProjects = await groupProjectDAL.find({ groupId: group.id });
|
||||
await Promise.allSettled([
|
||||
...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId))
|
||||
]);
|
||||
|
||||
return users[0];
|
||||
};
|
||||
|
||||
@@ -479,6 +502,11 @@ export const groupServiceFactory = ({
|
||||
projectKeyDAL
|
||||
});
|
||||
|
||||
const groupProjects = await groupProjectDAL.find({ groupId: group.id });
|
||||
await Promise.allSettled([
|
||||
...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId))
|
||||
]);
|
||||
|
||||
return users[0];
|
||||
};
|
||||
|
||||
|
||||
+15
-1
@@ -28,7 +28,7 @@ type TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep = {
|
||||
identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory;
|
||||
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "invalidateProjectPermissionCache">;
|
||||
};
|
||||
|
||||
export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType<
|
||||
@@ -115,6 +115,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
permissions: packedPermission
|
||||
});
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
|
||||
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
@@ -132,6 +134,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
|
||||
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||
});
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
|
||||
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
@@ -224,6 +229,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
|
||||
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
||||
});
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
|
||||
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
@@ -239,6 +247,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
temporaryRange: null,
|
||||
temporaryMode: null
|
||||
});
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
|
||||
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
@@ -294,6 +305,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
});
|
||||
|
||||
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
|
||||
|
||||
return {
|
||||
...deletedPrivilege,
|
||||
permissions: unpackPermissions(deletedPrivilege.permissions)
|
||||
|
||||
+16
-2
@@ -31,7 +31,7 @@ type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = {
|
||||
identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory;
|
||||
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "invalidateProjectPermissionCache">;
|
||||
};
|
||||
|
||||
export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
|
||||
@@ -129,6 +129,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
slug,
|
||||
permissions: packedPermission
|
||||
});
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
|
||||
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
@@ -146,6 +149,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
|
||||
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||
});
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
|
||||
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
@@ -250,6 +256,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
|
||||
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
||||
});
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
|
||||
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
@@ -265,6 +274,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
temporaryRange: null,
|
||||
temporaryMode: null
|
||||
});
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
|
||||
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
@@ -338,9 +350,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
}
|
||||
|
||||
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
|
||||
|
||||
return {
|
||||
...deletedPrivilege,
|
||||
|
||||
permissions: unpackPermissions(deletedPrivilege.permissions)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ export const getDefaultOnPremFeatures = () => {
|
||||
caCrl: false,
|
||||
sshHostGroups: false,
|
||||
enterpriseSecretSyncs: false,
|
||||
enterpriseCertificateSyncs: false,
|
||||
enterpriseAppConnections: true,
|
||||
machineIdentityAuthTemplates: false
|
||||
};
|
||||
|
||||
@@ -62,6 +62,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
sshHostGroups: false,
|
||||
secretScanning: false,
|
||||
enterpriseSecretSyncs: false,
|
||||
enterpriseCertificateSyncs: false,
|
||||
enterpriseAppConnections: false,
|
||||
fips: false,
|
||||
eventSubscriptions: false,
|
||||
|
||||
@@ -75,6 +75,7 @@ export type TFeatureSet = {
|
||||
sshHostGroups: false;
|
||||
secretScanning: false;
|
||||
enterpriseSecretSyncs: false;
|
||||
enterpriseCertificateSyncs: false;
|
||||
enterpriseAppConnections: false;
|
||||
machineIdentityAuthTemplates: false;
|
||||
fips: false;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ProjectPermissionKmipActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionPkiSyncActions,
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretEventActions,
|
||||
@@ -209,6 +210,19 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionPkiSyncActions.Create,
|
||||
ProjectPermissionPkiSyncActions.Edit,
|
||||
ProjectPermissionPkiSyncActions.Delete,
|
||||
ProjectPermissionPkiSyncActions.Read,
|
||||
ProjectPermissionPkiSyncActions.SyncCertificates,
|
||||
ProjectPermissionPkiSyncActions.ImportCertificates,
|
||||
ProjectPermissionPkiSyncActions.RemoveCertificates
|
||||
],
|
||||
ProjectPermissionSub.PkiSyncs
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionKmipActions.CreateClients,
|
||||
@@ -462,6 +476,19 @@ const buildMemberPermissionRules = () => {
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionPkiSyncActions.Create,
|
||||
ProjectPermissionPkiSyncActions.Edit,
|
||||
ProjectPermissionPkiSyncActions.Delete,
|
||||
ProjectPermissionPkiSyncActions.Read,
|
||||
ProjectPermissionPkiSyncActions.SyncCertificates,
|
||||
ProjectPermissionPkiSyncActions.ImportCertificates,
|
||||
ProjectPermissionPkiSyncActions.RemoveCertificates
|
||||
],
|
||||
ProjectPermissionSub.PkiSyncs
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretScanningDataSourceActions.Read,
|
||||
@@ -526,6 +553,7 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||
can(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs);
|
||||
can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
||||
|
||||
can(
|
||||
|
||||
@@ -58,6 +58,13 @@ export enum OrgPermissionGatewayActions {
|
||||
AttachGateways = "attach-gateways"
|
||||
}
|
||||
|
||||
export enum OrgPermissionRelayActions {
|
||||
CreateRelays = "create-relays",
|
||||
ListRelays = "list-relays",
|
||||
EditRelays = "edit-relays",
|
||||
DeleteRelays = "delete-relays"
|
||||
}
|
||||
|
||||
export enum OrgPermissionIdentityActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
@@ -109,6 +116,7 @@ export enum OrgPermissionSubjects {
|
||||
AppConnections = "app-connections",
|
||||
Kmip = "kmip",
|
||||
Gateway = "gateway",
|
||||
Relay = "relay",
|
||||
SecretShare = "secret-share"
|
||||
}
|
||||
|
||||
@@ -136,6 +144,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
|
||||
| [OrgPermissionRelayActions, OrgPermissionSubjects.Relay]
|
||||
| [
|
||||
OrgPermissionAppConnectionActions,
|
||||
(
|
||||
@@ -279,6 +288,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(OrgPermissionSubjects.Relay).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionRelayActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -383,6 +398,11 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
|
||||
|
||||
can(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
|
||||
can(OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay);
|
||||
can(OrgPermissionRelayActions.EditRelays, OrgPermissionSubjects.Relay);
|
||||
can(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
|
||||
@@ -445,6 +465,10 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
|
||||
|
||||
can(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
|
||||
can(OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay);
|
||||
can(OrgPermissionRelayActions.EditRelays, OrgPermissionSubjects.Relay);
|
||||
|
||||
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
|
||||
can(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { MongoQuery } from "@ucast/mongo2js";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
@@ -283,4 +284,5 @@ export type TPermissionServiceFactory = {
|
||||
projectId: string;
|
||||
checkPermissions: ProjectPermissionSet;
|
||||
}) => Promise<boolean>;
|
||||
invalidateProjectPermissionCache: (projectId: string, tx?: Knex) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import { MongoQuery } from "@ucast/mongo2js";
|
||||
import handlebars from "handlebars";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import {
|
||||
ActionProjectType,
|
||||
@@ -20,9 +21,11 @@ import {
|
||||
projectViewerPermission,
|
||||
sshHostBootstrapPermissions
|
||||
} from "@app/ee/services/permission/default-roles";
|
||||
import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { conditionsMatcher } from "@app/lib/casl";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { objectify } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@@ -49,6 +52,7 @@ type TPermissionServiceFactoryDep = {
|
||||
serviceTokenDAL: Pick<TServiceTokenDALFactory, "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
permissionDAL: TPermissionDALFactory;
|
||||
keyStore: TKeyStoreFactory;
|
||||
};
|
||||
|
||||
export const permissionServiceFactory = ({
|
||||
@@ -56,7 +60,8 @@ export const permissionServiceFactory = ({
|
||||
orgRoleDAL,
|
||||
projectRoleDAL,
|
||||
serviceTokenDAL,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
keyStore
|
||||
}: TPermissionServiceFactoryDep): TPermissionServiceFactory => {
|
||||
const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => {
|
||||
const rules = orgUserRoles
|
||||
@@ -83,6 +88,68 @@ export const permissionServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const invalidateProjectPermissionCache = async (projectId: string, tx?: Knex) => {
|
||||
const projectPermissionDalVersionKey = KeyStorePrefixes.ProjectPermissionDalVersion(projectId);
|
||||
await keyStore.pgIncrementBy(projectPermissionDalVersionKey, {
|
||||
incr: 1,
|
||||
tx,
|
||||
expiry: KeyStoreTtls.ProjectPermissionDalVersionTtl
|
||||
});
|
||||
};
|
||||
|
||||
const calculateProjectPermissionTtl = (membership: unknown): number => {
|
||||
const now = new Date();
|
||||
let minTtl = KeyStoreTtls.ProjectPermissionCacheInSeconds;
|
||||
|
||||
const getMinEndTime = (items: Array<{ temporaryAccessEndTime?: Date | null; isTemporary?: boolean }>) => {
|
||||
return items
|
||||
.filter((item) => item.isTemporary && item.temporaryAccessEndTime)
|
||||
.map((item) => item.temporaryAccessEndTime!)
|
||||
.filter((endTime) => endTime > now)
|
||||
.reduce((min, endTime) => (!min || endTime < min ? endTime : min), null as Date | null);
|
||||
};
|
||||
|
||||
const roleTimes: Date[] = [];
|
||||
const additionalPrivilegeTimes: Date[] = [];
|
||||
|
||||
if (
|
||||
membership &&
|
||||
typeof membership === "object" &&
|
||||
"roles" in membership &&
|
||||
Array.isArray((membership as Record<string, unknown>).roles)
|
||||
) {
|
||||
const roles = (membership as Record<string, unknown>).roles as Array<{
|
||||
temporaryAccessEndTime?: Date | null;
|
||||
isTemporary?: boolean;
|
||||
}>;
|
||||
const minRoleEndTime = getMinEndTime(roles);
|
||||
if (minRoleEndTime) roleTimes.push(minRoleEndTime);
|
||||
}
|
||||
|
||||
if (
|
||||
membership &&
|
||||
typeof membership === "object" &&
|
||||
"additionalPrivileges" in membership &&
|
||||
Array.isArray((membership as Record<string, unknown>).additionalPrivileges)
|
||||
) {
|
||||
const additionalPrivileges = (membership as Record<string, unknown>).additionalPrivileges as Array<{
|
||||
temporaryAccessEndTime?: Date | null;
|
||||
isTemporary?: boolean;
|
||||
}>;
|
||||
const minAdditionalEndTime = getMinEndTime(additionalPrivileges);
|
||||
if (minAdditionalEndTime) additionalPrivilegeTimes.push(minAdditionalEndTime);
|
||||
}
|
||||
|
||||
const allEndTimes = [...roleTimes, ...additionalPrivilegeTimes];
|
||||
if (allEndTimes.length > 0) {
|
||||
const nearestEndTime = allEndTimes.reduce((min, endTime) => (!min || endTime < min ? endTime : min));
|
||||
const timeUntilExpiry = Math.floor((nearestEndTime.getTime() - now.getTime()) / 1000);
|
||||
minTtl = Math.min(minTtl, Math.max(1, timeUntilExpiry));
|
||||
}
|
||||
|
||||
return minTtl;
|
||||
};
|
||||
|
||||
const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
|
||||
const rules = projectUserRoles
|
||||
.map(({ role, permissions }) => {
|
||||
@@ -577,35 +644,94 @@ export const permissionServiceFactory = ({
|
||||
actorId = assumedPrivilegeDetailsCtx.actorId;
|
||||
}
|
||||
|
||||
if (actor === ActorType.SERVICE) {
|
||||
return getServiceTokenProjectPermission({
|
||||
serviceTokenId: actorId,
|
||||
projectId,
|
||||
actorOrgId,
|
||||
actionProjectType
|
||||
}) as Promise<TProjectPermissionRT<T>>;
|
||||
}
|
||||
|
||||
const cachedProjectPermissionVersion = await keyStore.pgGetIntItem(
|
||||
KeyStorePrefixes.ProjectPermissionDalVersion(projectId)
|
||||
);
|
||||
const projectPermissionVersion = Number(cachedProjectPermissionVersion || 0);
|
||||
const cacheKey = KeyStorePrefixes.ProjectPermission(
|
||||
projectId,
|
||||
projectPermissionVersion,
|
||||
actor,
|
||||
actorId,
|
||||
actionProjectType || ActionProjectType.Any
|
||||
);
|
||||
|
||||
try {
|
||||
const cachedData = await keyStore.getItem(cacheKey);
|
||||
if (cachedData) {
|
||||
const parsed = JSON.parse(cachedData) as {
|
||||
rules: RawRuleOf<MongoAbility<ProjectPermissionSet>>[];
|
||||
membership: {
|
||||
roles?: Array<{ role: string; customRoleSlug?: string }>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
const permission = createMongoAbility<ProjectPermissionSet>(parsed.rules, {
|
||||
conditionsMatcher
|
||||
});
|
||||
|
||||
return {
|
||||
permission,
|
||||
membership: parsed.membership,
|
||||
hasRole: (role: string) =>
|
||||
parsed.membership.roles?.findIndex(
|
||||
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
|
||||
) !== -1
|
||||
} as TProjectPermissionRT<T>;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to get project permission");
|
||||
}
|
||||
|
||||
let result: TProjectPermissionRT<T>;
|
||||
|
||||
switch (actor) {
|
||||
case ActorType.USER:
|
||||
return getUserProjectPermission({
|
||||
result = (await getUserProjectPermission({
|
||||
userId: actorId,
|
||||
projectId,
|
||||
authMethod: actorAuthMethod,
|
||||
userOrgId: actorOrgId,
|
||||
actionProjectType
|
||||
}) as Promise<TProjectPermissionRT<T>>;
|
||||
case ActorType.SERVICE:
|
||||
return getServiceTokenProjectPermission({
|
||||
serviceTokenId: actorId,
|
||||
projectId,
|
||||
actorOrgId,
|
||||
actionProjectType
|
||||
}) as Promise<TProjectPermissionRT<T>>;
|
||||
})) as TProjectPermissionRT<T>;
|
||||
break;
|
||||
case ActorType.IDENTITY:
|
||||
return getIdentityProjectPermission({
|
||||
result = (await getIdentityProjectPermission({
|
||||
identityId: actorId,
|
||||
projectId,
|
||||
identityOrgId: actorOrgId,
|
||||
actionProjectType
|
||||
}) as Promise<TProjectPermissionRT<T>>;
|
||||
})) as TProjectPermissionRT<T>;
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestError({
|
||||
message: "Invalid actor provided",
|
||||
name: "Get project permission"
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const cacheData = {
|
||||
rules: result.permission.rules,
|
||||
membership: result.membership
|
||||
};
|
||||
|
||||
const ttl = calculateProjectPermissionTtl(result.membership);
|
||||
await keyStore.setItemWithExpiry(cacheKey, ttl, JSON.stringify(cacheData));
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to cache project permission");
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const getProjectPermissionByRole: TPermissionServiceFactory["getProjectPermissionByRole"] = async (
|
||||
@@ -668,6 +794,7 @@ export const permissionServiceFactory = ({
|
||||
getProjectPermissionByRole,
|
||||
buildOrgPermission,
|
||||
buildProjectPermissionRules,
|
||||
checkGroupProjectPermission
|
||||
checkGroupProjectPermission,
|
||||
invalidateProjectPermissionCache
|
||||
};
|
||||
};
|
||||
|
||||
@@ -120,6 +120,16 @@ export enum ProjectPermissionSecretSyncActions {
|
||||
RemoveSecrets = "remove-secrets"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionPkiSyncActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
SyncCertificates = "sync-certificates",
|
||||
ImportCertificates = "import-certificates",
|
||||
RemoveCertificates = "remove-certificates"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretRotationActions {
|
||||
Read = "read",
|
||||
ReadGeneratedCredentials = "read-generated-credentials",
|
||||
@@ -212,6 +222,7 @@ export enum ProjectPermissionSub {
|
||||
Kms = "kms",
|
||||
Cmek = "cmek",
|
||||
SecretSyncs = "secret-syncs",
|
||||
PkiSyncs = "pki-syncs",
|
||||
Kmip = "kmip",
|
||||
SecretScanningDataSources = "secret-scanning-data-sources",
|
||||
SecretScanningFindings = "secret-scanning-findings",
|
||||
@@ -244,6 +255,10 @@ export type SecretSyncSubjectFields = {
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
export type PkiSyncSubjectFields = {
|
||||
subscriberName: string;
|
||||
};
|
||||
|
||||
export type DynamicSecretSubjectFields = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
@@ -308,6 +323,10 @@ export type ProjectPermissionSet =
|
||||
ProjectPermissionSecretSyncActions,
|
||||
ProjectPermissionSub.SecretSyncs | (ForcedSubject<ProjectPermissionSub.SecretSyncs> & SecretSyncSubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionPkiSyncActions,
|
||||
ProjectPermissionSub.PkiSyncs | (ForcedSubject<ProjectPermissionSub.PkiSyncs> & PkiSyncSubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
(
|
||||
@@ -480,6 +499,22 @@ const SecretSyncConditionV2Schema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
const PkiSyncConditionSchema = z
|
||||
.object({
|
||||
subscriberName: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
|
||||
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
|
||||
})
|
||||
.partial()
|
||||
])
|
||||
})
|
||||
.partial();
|
||||
|
||||
const SecretImportConditionSchema = z
|
||||
.object({
|
||||
environment: z.union([
|
||||
@@ -943,6 +978,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.PkiSyncs).describe("The entity this permission pertains to."),
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPkiSyncActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
conditions: PkiSyncConditionSchema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretEvents).describe("The entity this permission pertains to."),
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
|
||||
@@ -754,7 +754,8 @@ export const pitServiceFactory = ({
|
||||
secrets: newSecrets.map((secret) => ({
|
||||
secretId: secret.id,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version
|
||||
secretVersion: secret.version,
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
});
|
||||
@@ -781,7 +782,8 @@ export const pitServiceFactory = ({
|
||||
secrets: updatedSecrets.map((secret) => ({
|
||||
secretId: secret.id,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version
|
||||
secretVersion: secret.version,
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
+15
-1
@@ -27,7 +27,7 @@ import {
|
||||
type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
|
||||
projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById" | "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "invalidateProjectPermissionCache">;
|
||||
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update">;
|
||||
};
|
||||
|
||||
@@ -115,6 +115,9 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
slug,
|
||||
permissions: packedPermission
|
||||
});
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
|
||||
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
@@ -133,6 +136,9 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
|
||||
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
|
||||
});
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
|
||||
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
@@ -230,6 +236,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
||||
});
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
|
||||
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
@@ -245,6 +253,9 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
temporaryRange: null,
|
||||
temporaryMode: null
|
||||
});
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
|
||||
|
||||
return {
|
||||
...additionalPrivilege,
|
||||
permissions: unpackPermissions(additionalPrivilege.permissions)
|
||||
@@ -291,6 +302,9 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
}
|
||||
);
|
||||
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
|
||||
|
||||
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
|
||||
|
||||
return {
|
||||
...deletedPrivilege,
|
||||
permissions: unpackPermissions(deletedPrivilege.permissions)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const RELAY_CONNECTING_GATEWAY_INFO = "1.3.6.1.4.1.12345.100.3";
|
||||
@@ -1,9 +1,13 @@
|
||||
import { isIP } from "node:net";
|
||||
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { TRelays } from "@app/db/schemas";
|
||||
import { PgSqlLock } from "@app/keystore/keystore";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
@@ -14,11 +18,15 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret/dynamic-secret-fns";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionRelayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||
import { createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns";
|
||||
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
|
||||
import { SshCertKeyAlgorithm } from "../ssh-certificate/ssh-certificate-types";
|
||||
import { TInstanceRelayConfigDALFactory } from "./instance-relay-config-dal";
|
||||
import { TOrgRelayConfigDALFactory } from "./org-relay-config-dal";
|
||||
import { RELAY_CONNECTING_GATEWAY_INFO } from "./relay-constants";
|
||||
import { TRelayDALFactory } from "./relay-dal";
|
||||
|
||||
export type TRelayServiceFactory = ReturnType<typeof relayServiceFactory>;
|
||||
@@ -29,12 +37,16 @@ export const relayServiceFactory = ({
|
||||
instanceRelayConfigDAL,
|
||||
orgRelayConfigDAL,
|
||||
relayDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
licenseService,
|
||||
permissionService
|
||||
}: {
|
||||
instanceRelayConfigDAL: TInstanceRelayConfigDALFactory;
|
||||
orgRelayConfigDAL: TOrgRelayConfigDALFactory;
|
||||
relayDAL: TRelayDALFactory;
|
||||
kmsService: TKmsServiceFactory;
|
||||
licenseService: TLicenseServiceFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
}) => {
|
||||
const $getInstanceCAs = async () => {
|
||||
const instanceConfig = await instanceRelayConfigDAL.transaction(async (tx) => {
|
||||
@@ -639,8 +651,9 @@ export const relayServiceFactory = ({
|
||||
true
|
||||
),
|
||||
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true),
|
||||
|
||||
// san
|
||||
new x509.SubjectAlternativeNameExtension([{ type: "ip", value: host }], false)
|
||||
new x509.SubjectAlternativeNameExtension([{ type: isIP(host) ? "ip" : "dns", value: host }], false)
|
||||
];
|
||||
|
||||
const relayServerSerialNumber = createSerialNumber();
|
||||
@@ -689,6 +702,7 @@ export const relayServiceFactory = ({
|
||||
|
||||
const $generateRelayClientCredentials = async ({
|
||||
gatewayId,
|
||||
gatewayName,
|
||||
orgId,
|
||||
orgName,
|
||||
relayPkiClientCaCertificate,
|
||||
@@ -697,6 +711,7 @@ export const relayServiceFactory = ({
|
||||
relayPkiServerCaCertificateChain
|
||||
}: {
|
||||
gatewayId: string;
|
||||
gatewayName: string;
|
||||
orgId: string;
|
||||
orgName: string;
|
||||
relayPkiClientCaCertificate: Buffer;
|
||||
@@ -727,6 +742,16 @@ export const relayServiceFactory = ({
|
||||
const clientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey);
|
||||
const clientCertSerialNumber = createSerialNumber();
|
||||
|
||||
const connectingGatewayInfoExtension = new x509.Extension(
|
||||
RELAY_CONNECTING_GATEWAY_INFO,
|
||||
false,
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
name: gatewayName
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Build standard extensions
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
@@ -740,7 +765,8 @@ export const relayServiceFactory = ({
|
||||
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
|
||||
true
|
||||
),
|
||||
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true)
|
||||
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true),
|
||||
connectingGatewayInfoExtension
|
||||
];
|
||||
|
||||
const clientCert = await x509.X509CertificateGenerator.create({
|
||||
@@ -768,11 +794,13 @@ export const relayServiceFactory = ({
|
||||
const getCredentialsForGateway = async ({
|
||||
relayName,
|
||||
orgId,
|
||||
gatewayId
|
||||
gatewayId,
|
||||
gatewayName
|
||||
}: {
|
||||
relayName: string;
|
||||
orgId: string;
|
||||
gatewayId: string;
|
||||
gatewayName: string;
|
||||
}) => {
|
||||
let relay: TRelays | null = await relayDAL.findOne({
|
||||
orgId,
|
||||
@@ -819,10 +847,10 @@ export const relayServiceFactory = ({
|
||||
const relayClientSshCert = await createSshCert({
|
||||
caPrivateKey: orgCAs.relaySshClientCaPrivateKey.toString("utf8"),
|
||||
clientPublicKey: relayClientSshPublicKey,
|
||||
keyId: `relay-client-${relay.id}`,
|
||||
principals: [gatewayId],
|
||||
keyId: `client-${relayName}`,
|
||||
principals: [gatewayId, gatewayName],
|
||||
certType: SshCertType.USER,
|
||||
requestedTtl: "30d"
|
||||
requestedTtl: "1d"
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -837,12 +865,14 @@ export const relayServiceFactory = ({
|
||||
relayId,
|
||||
orgId,
|
||||
orgName,
|
||||
gatewayId
|
||||
gatewayId,
|
||||
gatewayName
|
||||
}: {
|
||||
relayId: string;
|
||||
orgId: string;
|
||||
orgName: string;
|
||||
gatewayId: string;
|
||||
gatewayName: string;
|
||||
}) => {
|
||||
const relay = await relayDAL.findOne({
|
||||
id: relayId
|
||||
@@ -860,6 +890,7 @@ export const relayServiceFactory = ({
|
||||
const instanceCAs = await $getInstanceCAs();
|
||||
const relayCertificateCredentials = await $generateRelayClientCredentials({
|
||||
gatewayId,
|
||||
gatewayName,
|
||||
orgId,
|
||||
orgName,
|
||||
relayPkiClientCaCertificate: instanceCAs.instanceRelayPkiClientCaCertificate,
|
||||
@@ -877,6 +908,7 @@ export const relayServiceFactory = ({
|
||||
const orgCAs = await $getOrgCAs(orgId);
|
||||
const relayCertificateCredentials = await $generateRelayClientCredentials({
|
||||
gatewayId,
|
||||
gatewayName,
|
||||
orgId,
|
||||
orgName,
|
||||
relayPkiClientCaCertificate: orgCAs.relayPkiClientCaCertificate,
|
||||
@@ -895,11 +927,13 @@ export const relayServiceFactory = ({
|
||||
host,
|
||||
name,
|
||||
identityId,
|
||||
actorAuthMethod,
|
||||
orgId
|
||||
}: {
|
||||
host: string;
|
||||
name: string;
|
||||
identityId?: string;
|
||||
actorAuthMethod?: ActorAuthMethod;
|
||||
orgId?: string;
|
||||
}) => {
|
||||
let relay: TRelays;
|
||||
@@ -908,6 +942,27 @@ export const relayServiceFactory = ({
|
||||
await verifyHostInputValidity(host);
|
||||
|
||||
if (isOrgRelay) {
|
||||
const orgLicensePlan = await licenseService.getPlan(orgId);
|
||||
if (!orgLicensePlan.gateway) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Relay registration failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityId,
|
||||
orgId,
|
||||
actorAuthMethod!,
|
||||
orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionRelayActions.CreateRelays,
|
||||
OrgPermissionSubjects.Relay
|
||||
);
|
||||
|
||||
relay = await relayDAL.transaction(async (tx) => {
|
||||
const existingRelay = await relayDAL.findOne(
|
||||
{
|
||||
@@ -995,9 +1050,75 @@ export const relayServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const getRelays = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: {
|
||||
actorId: string;
|
||||
actor: ActorType;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
}) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
|
||||
|
||||
const instanceRelays = await relayDAL.find({
|
||||
orgId: null
|
||||
});
|
||||
|
||||
const orgRelays = await relayDAL.find({
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
return [...instanceRelays, ...orgRelays];
|
||||
};
|
||||
|
||||
const deleteRelay = async ({
|
||||
id,
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: {
|
||||
id: string;
|
||||
actorId: string;
|
||||
actor: ActorType;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
}) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
|
||||
|
||||
const relay = await relayDAL.findById(id);
|
||||
if (!relay || relay.orgId !== actorOrgId || relay.orgId === null) {
|
||||
throw new NotFoundError({ message: "Relay not found" });
|
||||
}
|
||||
|
||||
const deletedRelay = await relayDAL.deleteById(id);
|
||||
return deletedRelay;
|
||||
};
|
||||
|
||||
return {
|
||||
registerRelay,
|
||||
getCredentialsForGateway,
|
||||
getCredentialsForClient
|
||||
getCredentialsForClient,
|
||||
getRelays,
|
||||
deleteRelay
|
||||
};
|
||||
};
|
||||
|
||||
+12
-3
@@ -284,7 +284,8 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
db.ref("version").withSchema(TableName.SecretVersionV2).as("secVerVersion"),
|
||||
db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"),
|
||||
db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"),
|
||||
db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment")
|
||||
db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment"),
|
||||
db.ref("skipMultilineEncoding").withSchema(TableName.SecretVersionV2).as("secVerSkipMultilineEncoding")
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||
@@ -326,14 +327,22 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "secretVersion",
|
||||
label: "secretVersion" as const,
|
||||
mapper: ({ secretVersion, secVerVersion, secVerKey, secVerValue, secVerComment }) =>
|
||||
mapper: ({
|
||||
secretVersion,
|
||||
secVerVersion,
|
||||
secVerKey,
|
||||
secVerValue,
|
||||
secVerComment,
|
||||
secVerSkipMultilineEncoding
|
||||
}) =>
|
||||
secretVersion
|
||||
? {
|
||||
version: secVerVersion,
|
||||
id: secretVersion,
|
||||
key: secVerKey,
|
||||
encryptedValue: secVerValue,
|
||||
encryptedComment: secVerComment
|
||||
encryptedComment: secVerComment,
|
||||
skipMultilineEncoding: secVerSkipMultilineEncoding
|
||||
}
|
||||
: undefined,
|
||||
childrenMapper: [
|
||||
|
||||
+41
-55
@@ -285,13 +285,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
) {
|
||||
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||
}
|
||||
const getHasSecretReadAccess = (
|
||||
shouldCheckSecretPermission: boolean | null | undefined,
|
||||
environment: string,
|
||||
tags: { slug: string }[],
|
||||
secretPath?: string
|
||||
) => {
|
||||
if (shouldCheckSecretPermission) {
|
||||
const getHasSecretReadAccess = (environment: string, tags: { slug: string }[], secretPath?: string) => {
|
||||
const isReviewer = policy.approvers.some(({ userId }) => userId === actorId);
|
||||
|
||||
if (!isReviewer) {
|
||||
const canRead = hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||
environment,
|
||||
secretPath: secretPath || "/",
|
||||
@@ -322,44 +319,33 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
version: el.version,
|
||||
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
||||
secretValueHidden: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
),
|
||||
secretValue: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
)
|
||||
secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
|
||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||
: el.secret && el.secret.isRotatedSecret
|
||||
? undefined
|
||||
: el.encryptedValue
|
||||
: el.encryptedValue !== undefined && el.encryptedValue !== null
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: "",
|
||||
secretComment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: "",
|
||||
: undefined,
|
||||
secretComment:
|
||||
el.encryptedComment !== undefined && el.encryptedComment !== null
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined,
|
||||
skipMultilineEncoding:
|
||||
el.skipMultilineEncoding !== undefined && el.skipMultilineEncoding !== null
|
||||
? el.skipMultilineEncoding
|
||||
: undefined,
|
||||
secret: el.secret
|
||||
? {
|
||||
secretKey: el.secret.key,
|
||||
id: el.secret.id,
|
||||
version: el.secret.version,
|
||||
secretValueHidden: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
),
|
||||
secretValue: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
)
|
||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||
: el.secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
||||
@@ -375,17 +361,11 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
id: el.secretVersion.id,
|
||||
version: el.secretVersion.version,
|
||||
secretValueHidden: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
),
|
||||
secretValue: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
)
|
||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||
: el.secretVersion.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
||||
@@ -394,7 +374,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
||||
: "",
|
||||
tags: el.secretVersion.tags,
|
||||
secretMetadata: el.oldSecretMetadata as ResourceMetadataDTO
|
||||
secretMetadata: el.oldSecretMetadata as ResourceMetadataDTO,
|
||||
skipMultilineEncoding: el.secretVersion.skipMultilineEncoding
|
||||
}
|
||||
: undefined
|
||||
}));
|
||||
@@ -403,12 +384,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
||||
secrets = encryptedSecrets.map((el) => ({
|
||||
...el,
|
||||
secretValueHidden: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
),
|
||||
secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
|
||||
...decryptSecretWithBot(el, botKey),
|
||||
secret: el.secret
|
||||
? {
|
||||
@@ -733,9 +709,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
tx,
|
||||
inputSecrets: secretUpdationCommits.map((el) => {
|
||||
const encryptedValue =
|
||||
!el.secret?.isRotatedSecret && typeof el.encryptedValue !== "undefined"
|
||||
!el.secret?.isRotatedSecret && el.encryptedValue !== null && el.encryptedValue !== undefined
|
||||
? {
|
||||
encryptedValue: el.encryptedValue as Buffer,
|
||||
encryptedValue: el.encryptedValue,
|
||||
references: el.encryptedValue
|
||||
? getAllSecretReferencesV2Bridge(
|
||||
secretManagerDecryptor({
|
||||
@@ -749,9 +725,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
filter: { id: el.secretId as string, type: SecretType.Shared },
|
||||
data: {
|
||||
reminderRepeatDays: el.reminderRepeatDays,
|
||||
encryptedComment: el.encryptedComment,
|
||||
encryptedComment: el.encryptedComment !== null ? el.encryptedComment : undefined,
|
||||
reminderNote: el.reminderNote,
|
||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||
skipMultilineEncoding: el.skipMultilineEncoding !== null ? el.skipMultilineEncoding : undefined,
|
||||
key: el.key,
|
||||
tags: el?.tags.map(({ id }) => id),
|
||||
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||
@@ -1084,7 +1060,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
});
|
||||
@@ -1100,7 +1078,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1119,7 +1099,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
});
|
||||
@@ -1135,7 +1117,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1625,11 +1609,13 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
key: newSecretName || secretKey,
|
||||
encryptedComment: setKnexStringValue(
|
||||
secretComment,
|
||||
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
||||
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob,
|
||||
true // scott: we need to encrypt empty string on update to differentiate not updating comment vs clearing comment
|
||||
),
|
||||
encryptedValue: setKnexStringValue(
|
||||
secretValue,
|
||||
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
||||
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob,
|
||||
true // scott: we need to encrypt empty string on update to differentiate not updating value vs clearing value
|
||||
),
|
||||
reminderRepeatDays,
|
||||
reminderNote,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./redis-credentials-rotation-constants";
|
||||
export * from "./redis-credentials-rotation-fns";
|
||||
export * from "./redis-credentials-rotation-schemas";
|
||||
export * from "./redis-credentials-rotation-types";
|
||||
+15
@@ -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 REDIS_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||
name: "Redis Credentials",
|
||||
type: SecretRotation.RedisCredentials,
|
||||
connection: AppConnection.Redis,
|
||||
template: {
|
||||
secretsMapping: {
|
||||
username: "REDIS_USERNAME",
|
||||
password: "REDIS_PASSWORD"
|
||||
}
|
||||
}
|
||||
};
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import Redis from "ioredis";
|
||||
|
||||
import {
|
||||
TRotationFactory,
|
||||
TRotationFactoryGetSecretsPayload,
|
||||
TRotationFactoryIssueCredentials,
|
||||
TRotationFactoryRevokeCredentials,
|
||||
TRotationFactoryRotateCredentials
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { verifyHostInputValidity } from "../../dynamic-secret/dynamic-secret-fns";
|
||||
import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../shared/utils";
|
||||
import {
|
||||
TRedisCredentialsRotationGeneratedCredentials,
|
||||
TRedisCredentialsRotationWithConnection
|
||||
} from "./redis-credentials-rotation-types";
|
||||
|
||||
const redactPasswords = (e: unknown, credentials: TRedisCredentialsRotationGeneratedCredentials) => {
|
||||
const error = e as Error;
|
||||
|
||||
if (!error?.message) return "Unknown error";
|
||||
|
||||
let redactedMessage = error.message;
|
||||
|
||||
credentials.forEach(({ password }) => {
|
||||
redactedMessage = redactedMessage.replaceAll(password, "*******************");
|
||||
});
|
||||
|
||||
return redactedMessage;
|
||||
};
|
||||
|
||||
export const redisCredentialsRotationFactory: TRotationFactory<
|
||||
TRedisCredentialsRotationWithConnection,
|
||||
TRedisCredentialsRotationGeneratedCredentials
|
||||
> = (secretRotation) => {
|
||||
const { connection, secretsMapping, parameters } = secretRotation;
|
||||
|
||||
const $getClient = async () => {
|
||||
const [hostIp] = await verifyHostInputValidity(connection.credentials.host);
|
||||
|
||||
let conn: Redis | null = null;
|
||||
try {
|
||||
conn = new Redis({
|
||||
username: connection.credentials.username,
|
||||
host: hostIp,
|
||||
port: connection.credentials.port,
|
||||
password: connection.credentials.password,
|
||||
...(connection.credentials.sslEnabled && {
|
||||
tls: {
|
||||
rejectUnauthorized: connection.credentials.sslRejectUnauthorized,
|
||||
ca: connection.credentials.sslCertificate
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let result: string;
|
||||
if (connection.credentials.password) {
|
||||
result = await conn.auth(connection.credentials.username, connection.credentials.password, () => {});
|
||||
} else {
|
||||
result = await conn.auth(connection.credentials.username, () => {});
|
||||
}
|
||||
|
||||
if (result !== "OK") {
|
||||
throw new BadRequestError({ message: `Invalid credentials, Redis returned ${result} status` });
|
||||
}
|
||||
|
||||
return conn;
|
||||
} catch (err) {
|
||||
if (conn) await conn.quit();
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new user and password for the redis user using ACL
|
||||
*/
|
||||
const $rotateAclUser = async () => {
|
||||
let client: Redis | null = null;
|
||||
|
||||
const username = generatePassword({
|
||||
length: 32,
|
||||
required: {
|
||||
symbols: 0,
|
||||
digits: 5,
|
||||
uppercase: 5,
|
||||
lowercase: 5
|
||||
}
|
||||
});
|
||||
|
||||
const password = generatePassword(parameters.passwordRequirements || DEFAULT_PASSWORD_REQUIREMENTS);
|
||||
|
||||
try {
|
||||
client = await $getClient();
|
||||
|
||||
// important: permissionScope is user input so we need to sanitize it, which we do by splitting the permission scope into parts and then passing them to the ACL command as separate arguments
|
||||
const permissionParts = parameters.permissionScope.split(" ");
|
||||
await client.call("ACL", "SETUSER", username, `>${password}`, "on", ...permissionParts);
|
||||
|
||||
return {
|
||||
username,
|
||||
password
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to rotate credentials: ${redactPasswords(error, [{ username, password }])}`
|
||||
});
|
||||
} finally {
|
||||
if (client) await client.quit();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Revokes a ACL password from the Redis server using its username and password.
|
||||
*/
|
||||
const revokeCredential = async (username: string) => {
|
||||
let client: Redis | null = null;
|
||||
|
||||
try {
|
||||
client = await $getClient();
|
||||
await client.call("ACL", "DELUSER", username);
|
||||
} catch (error: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to revoke credential: ${redactPasswords(error, [{ username, password: username }])}`
|
||||
});
|
||||
} finally {
|
||||
if (client) await client.quit();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Issues a new set of credentials.
|
||||
*/
|
||||
const issueCredentials: TRotationFactoryIssueCredentials<TRedisCredentialsRotationGeneratedCredentials> = async (
|
||||
callback
|
||||
) => {
|
||||
const credentials = await $rotateAclUser();
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
||||
/**
|
||||
* Revokes a list of credentials.
|
||||
*/
|
||||
const revokeCredentials: TRotationFactoryRevokeCredentials<TRedisCredentialsRotationGeneratedCredentials> = async (
|
||||
credentials,
|
||||
callback
|
||||
) => {
|
||||
if (!credentials?.length) return callback();
|
||||
|
||||
for (const { username } of credentials) {
|
||||
await revokeCredential(username);
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
return callback();
|
||||
};
|
||||
|
||||
/**
|
||||
* Rotates credentials by issuing new ones and revoking the old.
|
||||
*/
|
||||
const rotateCredentials: TRotationFactoryRotateCredentials<TRedisCredentialsRotationGeneratedCredentials> = async (
|
||||
oldCredentials,
|
||||
callback
|
||||
) => {
|
||||
const newCredentials = await $rotateAclUser();
|
||||
|
||||
if (oldCredentials?.username) {
|
||||
await revokeCredential(oldCredentials.username);
|
||||
}
|
||||
|
||||
return callback(newCredentials);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps the generated credentials into the secret payload format.
|
||||
*/
|
||||
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TRedisCredentialsRotationGeneratedCredentials> = ({
|
||||
username,
|
||||
password
|
||||
}) => [
|
||||
{ key: secretsMapping.username, value: username },
|
||||
{ key: secretsMapping.password, value: password }
|
||||
];
|
||||
|
||||
return {
|
||||
issueCredentials,
|
||||
revokeCredentials,
|
||||
rotateCredentials,
|
||||
getSecretsPayload
|
||||
};
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
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";
|
||||
|
||||
import { PasswordRequirementsSchema } from "../shared/general";
|
||||
|
||||
export const RedisCredentialsRotationGeneratedCredentialsSchema = z
|
||||
.object({
|
||||
username: z.string(),
|
||||
password: z.string()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.max(2);
|
||||
|
||||
const RedisCredentialsRotationSecretsMappingSchema = z.object({
|
||||
username: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.REDIS_CREDENTIALS.username),
|
||||
password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.REDIS_CREDENTIALS.password)
|
||||
});
|
||||
|
||||
export const RedisCredentialsRotationParametersSchema = z.object({
|
||||
passwordRequirements: PasswordRequirementsSchema.optional(),
|
||||
permissionScope: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Permission scope is required")
|
||||
.describe(SecretRotations.PARAMETERS.REDIS_CREDENTIALS.permissionScope)
|
||||
});
|
||||
|
||||
export const RedisCredentialsRotationTemplateSchema = z.object({
|
||||
secretsMapping: z.object({
|
||||
username: z.string(),
|
||||
password: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
export const RedisCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.RedisCredentials).extend({
|
||||
type: z.literal(SecretRotation.RedisCredentials),
|
||||
parameters: RedisCredentialsRotationParametersSchema,
|
||||
secretsMapping: RedisCredentialsRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const CreateRedisCredentialsRotationSchema = BaseCreateSecretRotationSchema(
|
||||
SecretRotation.RedisCredentials
|
||||
).extend({
|
||||
parameters: RedisCredentialsRotationParametersSchema,
|
||||
secretsMapping: RedisCredentialsRotationSecretsMappingSchema
|
||||
});
|
||||
|
||||
export const UpdateRedisCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
|
||||
SecretRotation.RedisCredentials
|
||||
).extend({
|
||||
parameters: RedisCredentialsRotationParametersSchema.optional(),
|
||||
secretsMapping: RedisCredentialsRotationSecretsMappingSchema.optional()
|
||||
});
|
||||
|
||||
export const RedisCredentialsRotationListItemSchema = z.object({
|
||||
name: z.literal("Redis Credentials"),
|
||||
connection: z.literal(AppConnection.Redis),
|
||||
type: z.literal(SecretRotation.RedisCredentials),
|
||||
template: RedisCredentialsRotationTemplateSchema
|
||||
});
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TRedisConnection } from "@app/services/app-connection/redis";
|
||||
|
||||
import {
|
||||
CreateRedisCredentialsRotationSchema,
|
||||
RedisCredentialsRotationGeneratedCredentialsSchema,
|
||||
RedisCredentialsRotationListItemSchema,
|
||||
RedisCredentialsRotationSchema
|
||||
} from "./redis-credentials-rotation-schemas";
|
||||
|
||||
export type TRedisCredentialsRotation = z.infer<typeof RedisCredentialsRotationSchema>;
|
||||
|
||||
export type TRedisCredentialsRotationInput = z.infer<typeof CreateRedisCredentialsRotationSchema>;
|
||||
|
||||
export type TRedisCredentialsRotationListItem = z.infer<typeof RedisCredentialsRotationListItemSchema>;
|
||||
|
||||
export type TRedisCredentialsRotationWithConnection = TRedisCredentialsRotation & {
|
||||
connection: TRedisConnection;
|
||||
};
|
||||
|
||||
export type TRedisCredentialsRotationGeneratedCredentials = z.infer<
|
||||
typeof RedisCredentialsRotationGeneratedCredentialsSchema
|
||||
>;
|
||||
@@ -7,7 +7,8 @@ export enum SecretRotation {
|
||||
AzureClientSecret = "azure-client-secret",
|
||||
AwsIamUserSecret = "aws-iam-user-secret",
|
||||
LdapPassword = "ldap-password",
|
||||
OktaClientSecret = "okta-client-secret"
|
||||
OktaClientSecret = "okta-client-secret",
|
||||
RedisCredentials = "redis-credentials"
|
||||
}
|
||||
|
||||
export enum SecretRotationStatus {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials";
|
||||
import { OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./okta-client-secret";
|
||||
import { ORACLEDB_CREDENTIALS_ROTATION_LIST_OPTION } from "./oracledb-credentials";
|
||||
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
||||
import { REDIS_CREDENTIALS_ROTATION_LIST_OPTION } from "./redis-credentials";
|
||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
|
||||
import { TSecretRotationV2ServiceFactory, TSecretRotationV2ServiceFactoryDep } from "./secret-rotation-v2-service";
|
||||
@@ -35,7 +36,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2List
|
||||
[SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.OktaClientSecret]: OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION
|
||||
[SecretRotation.OktaClientSecret]: OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.RedisCredentials]: REDIS_CREDENTIALS_ROTATION_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretRotationOptions = () => {
|
||||
|
||||
@@ -10,7 +10,8 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||
[SecretRotation.AzureClientSecret]: "Azure Client Secret",
|
||||
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret",
|
||||
[SecretRotation.LdapPassword]: "LDAP Password",
|
||||
[SecretRotation.OktaClientSecret]: "Okta Client Secret"
|
||||
[SecretRotation.OktaClientSecret]: "Okta Client Secret",
|
||||
[SecretRotation.RedisCredentials]: "Redis Credentials"
|
||||
};
|
||||
|
||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||
@@ -22,5 +23,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
|
||||
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
|
||||
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
|
||||
[SecretRotation.LdapPassword]: AppConnection.LDAP,
|
||||
[SecretRotation.OktaClientSecret]: AppConnection.Okta
|
||||
[SecretRotation.OktaClientSecret]: AppConnection.Okta,
|
||||
[SecretRotation.RedisCredentials]: AppConnection.Redis
|
||||
};
|
||||
|
||||
@@ -85,6 +85,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
|
||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||
import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns";
|
||||
import { oktaClientSecretRotationFactory } from "./okta-client-secret/okta-client-secret-rotation-fns";
|
||||
import { redisCredentialsRotationFactory } from "./redis-credentials/redis-credentials-rotation-fns";
|
||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||
|
||||
export type TSecretRotationV2ServiceFactoryDep = {
|
||||
@@ -132,7 +133,8 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
|
||||
[SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.OktaClientSecret]: oktaClientSecretRotationFactory as TRotationFactoryImplementation
|
||||
[SecretRotation.OktaClientSecret]: oktaClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||
[SecretRotation.RedisCredentials]: redisCredentialsRotationFactory as TRotationFactoryImplementation
|
||||
};
|
||||
|
||||
export const secretRotationV2ServiceFactory = ({
|
||||
|
||||
@@ -66,6 +66,13 @@ import {
|
||||
TPostgresCredentialsRotationListItem,
|
||||
TPostgresCredentialsRotationWithConnection
|
||||
} from "./postgres-credentials";
|
||||
import {
|
||||
TRedisCredentialsRotation,
|
||||
TRedisCredentialsRotationGeneratedCredentials,
|
||||
TRedisCredentialsRotationInput,
|
||||
TRedisCredentialsRotationListItem,
|
||||
TRedisCredentialsRotationWithConnection
|
||||
} from "./redis-credentials/redis-credentials-rotation-types";
|
||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||
import { SecretRotation } from "./secret-rotation-v2-enums";
|
||||
|
||||
@@ -78,7 +85,8 @@ export type TSecretRotationV2 =
|
||||
| TAzureClientSecretRotation
|
||||
| TLdapPasswordRotation
|
||||
| TAwsIamUserSecretRotation
|
||||
| TOktaClientSecretRotation;
|
||||
| TOktaClientSecretRotation
|
||||
| TRedisCredentialsRotation;
|
||||
|
||||
export type TSecretRotationV2WithConnection =
|
||||
| TPostgresCredentialsRotationWithConnection
|
||||
@@ -89,7 +97,8 @@ export type TSecretRotationV2WithConnection =
|
||||
| TAzureClientSecretRotationWithConnection
|
||||
| TLdapPasswordRotationWithConnection
|
||||
| TAwsIamUserSecretRotationWithConnection
|
||||
| TOktaClientSecretRotationWithConnection;
|
||||
| TOktaClientSecretRotationWithConnection
|
||||
| TRedisCredentialsRotationWithConnection;
|
||||
|
||||
export type TSecretRotationV2GeneratedCredentials =
|
||||
| TSqlCredentialsRotationGeneratedCredentials
|
||||
@@ -97,7 +106,8 @@ export type TSecretRotationV2GeneratedCredentials =
|
||||
| TAzureClientSecretRotationGeneratedCredentials
|
||||
| TLdapPasswordRotationGeneratedCredentials
|
||||
| TAwsIamUserSecretRotationGeneratedCredentials
|
||||
| TOktaClientSecretRotationGeneratedCredentials;
|
||||
| TOktaClientSecretRotationGeneratedCredentials
|
||||
| TRedisCredentialsRotationGeneratedCredentials;
|
||||
|
||||
export type TSecretRotationV2Input =
|
||||
| TPostgresCredentialsRotationInput
|
||||
@@ -108,7 +118,8 @@ export type TSecretRotationV2Input =
|
||||
| TAzureClientSecretRotationInput
|
||||
| TLdapPasswordRotationInput
|
||||
| TAwsIamUserSecretRotationInput
|
||||
| TOktaClientSecretRotationInput;
|
||||
| TOktaClientSecretRotationInput
|
||||
| TRedisCredentialsRotationInput;
|
||||
|
||||
export type TSecretRotationV2ListItem =
|
||||
| TPostgresCredentialsRotationListItem
|
||||
@@ -119,7 +130,8 @@ export type TSecretRotationV2ListItem =
|
||||
| TAzureClientSecretRotationListItem
|
||||
| TLdapPasswordRotationListItem
|
||||
| TAwsIamUserSecretRotationListItem
|
||||
| TOktaClientSecretRotationListItem;
|
||||
| TOktaClientSecretRotationListItem
|
||||
| TRedisCredentialsRotationListItem;
|
||||
|
||||
export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { MySqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation
|
||||
import { OktaClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
|
||||
import { OracleDBCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
|
||||
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
import { RedisCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/redis-credentials";
|
||||
|
||||
export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||
PostgresCredentialsRotationSchema,
|
||||
@@ -19,5 +20,6 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
||||
AzureClientSecretRotationSchema,
|
||||
LdapPasswordRotationSchema,
|
||||
AwsIamUserSecretRotationSchema,
|
||||
OktaClientSecretRotationSchema
|
||||
OktaClientSecretRotationSchema,
|
||||
RedisCredentialsRotationSchema
|
||||
]);
|
||||
|
||||
@@ -47,6 +47,7 @@ export const KeyStorePrefixes = {
|
||||
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
|
||||
PkiSyncLock: (syncId: string) => `pki-sync-mutex-${syncId}` as const,
|
||||
AppConnectionConcurrentJobs: (connectionId: string) => `app-connection-concurrency-${connectionId}` as const,
|
||||
SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const,
|
||||
SecretScanningLock: (dataSourceId: string, resourceExternalId: string) =>
|
||||
@@ -62,13 +63,28 @@ export const KeyStorePrefixes = {
|
||||
ActiveSSEConnectionsSet: (projectId: string, identityId: string) =>
|
||||
`sse-connections:${projectId}:${identityId}` as const,
|
||||
ActiveSSEConnections: (projectId: string, identityId: string, connectionId: string) =>
|
||||
`sse-connections:${projectId}:${identityId}:${connectionId}` as const
|
||||
`sse-connections:${projectId}:${identityId}:${connectionId}` as const,
|
||||
|
||||
ProjectPermission: (
|
||||
projectId: string,
|
||||
version: number,
|
||||
actorType: string,
|
||||
actorId: string,
|
||||
actionProjectType: string
|
||||
) => `project-permission:${projectId}:${version}:${actorType}:${actorId}:${actionProjectType}` as const,
|
||||
ProjectPermissionDalVersion: (projectId: string) => `project-permission:${projectId}:dal-version` as const,
|
||||
UserProjectPermissionPattern: (userId: string) => `project-permission:*:*:USER:${userId}:*` as const,
|
||||
IdentityProjectPermissionPattern: (identityId: string) => `project-permission:*:*:IDENTITY:${identityId}:*` as const,
|
||||
GroupMemberProjectPermissionPattern: (projectId: string, groupId: string) =>
|
||||
`group-member-project-permission:${projectId}:${groupId}:*` as const
|
||||
};
|
||||
|
||||
export const KeyStoreTtls = {
|
||||
SetSyncSecretIntegrationLastRunTimestampInSeconds: 60,
|
||||
SetSecretSyncLastRunTimestampInSeconds: 60,
|
||||
AccessTokenStatusUpdateInSeconds: 120
|
||||
AccessTokenStatusUpdateInSeconds: 120,
|
||||
ProjectPermissionCacheInSeconds: 300, // 5 minutes
|
||||
ProjectPermissionDalVersionTtl: "15m" // Project permission DAL version TTL
|
||||
};
|
||||
|
||||
type TDeleteItems = {
|
||||
|
||||
@@ -50,6 +50,7 @@ export enum ApiDocsTags {
|
||||
IdentitySpecificPrivilegesV2 = "Identity Specific Privileges V2",
|
||||
AppConnections = "App Connections",
|
||||
SecretSyncs = "Secret Syncs",
|
||||
PkiSyncs = "PKI Syncs",
|
||||
Integrations = "Integrations",
|
||||
ServiceTokens = "Service Tokens",
|
||||
AuditLogs = "Audit Logs",
|
||||
@@ -1048,7 +1049,7 @@ export const RAW_SECRETS = {
|
||||
},
|
||||
GET_ACCESS_LIST: {
|
||||
secretName: "The name of the secret to get the access list for.",
|
||||
workspaceId: "The ID of the project where the secret is located.",
|
||||
projectId: "The ID of the project where the secret is located.",
|
||||
environment: "The slug of the environment where the the secret is located.",
|
||||
secretPath: "The folder path where the secret is located."
|
||||
}
|
||||
@@ -2685,9 +2686,16 @@ export const SecretRotations = {
|
||||
},
|
||||
OKTA_CLIENT_SECRET: {
|
||||
clientId: "The ID of the Okta Application to rotate the client secret for."
|
||||
},
|
||||
REDIS_CREDENTIALS: {
|
||||
permissionScope: "The ACL permission scope to assign to the issued Redis users."
|
||||
}
|
||||
},
|
||||
SECRETS_MAPPING: {
|
||||
REDIS_CREDENTIALS: {
|
||||
username: "The name of the secret that the username will be mapped to.",
|
||||
password: "The name of the secret that the rotated password will be mapped to."
|
||||
},
|
||||
SQL_CREDENTIALS: {
|
||||
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."
|
||||
|
||||
@@ -129,6 +129,8 @@ const envSchema = z
|
||||
POSTHOG_HOST: zpStr(z.string().optional().default("https://app.posthog.com")),
|
||||
POSTHOG_PROJECT_API_KEY: zpStr(z.string().optional().default("phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE")),
|
||||
LOOPS_API_KEY: zpStr(z.string().optional()),
|
||||
// GitHub API token for upgrade path tool
|
||||
GITHUB_API_TOKEN: zpStr(z.string().optional()),
|
||||
// jwt options
|
||||
AUTH_SECRET: zpStr(z.string()).default(process.env.JWT_AUTH_SECRET), // for those still using old JWT_AUTH_SECRET
|
||||
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
|
||||
@@ -323,6 +325,10 @@ const envSchema = z
|
||||
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
// Heroku App Connection
|
||||
INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
// datadog
|
||||
SHOULD_USE_DATADOG_TRACER: zodStrBool.default("false"),
|
||||
DATADOG_PROFILING_ENABLED: zodStrBool.default("false"),
|
||||
@@ -433,7 +439,10 @@ const envSchema = z
|
||||
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID:
|
||||
data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET:
|
||||
data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET
|
||||
data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||
INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID: data.INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID || data.CLIENT_ID_HEROKU,
|
||||
INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET:
|
||||
data.INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET || data.CLIENT_SECRET_HEROKU
|
||||
}));
|
||||
|
||||
export type TEnvConfig = Readonly<z.infer<typeof envSchema>>;
|
||||
@@ -736,6 +745,19 @@ export const overwriteSchema: {
|
||||
description: "The Client Secret of your GCP OAuth2 application."
|
||||
}
|
||||
]
|
||||
},
|
||||
heroku: {
|
||||
name: "Heroku",
|
||||
fields: [
|
||||
{
|
||||
key: "INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID",
|
||||
description: "The Client ID of your Heroku application."
|
||||
},
|
||||
{
|
||||
key: "INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET",
|
||||
description: "The Client Secret of your Heroku application."
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,8 +16,12 @@ export const stripUndefinedInWhere = <T extends object>(val: T): Exclude<T, unde
|
||||
// if its undefined its skipped in knex
|
||||
// if its empty string its set as null
|
||||
// else pass to the required one
|
||||
export const setKnexStringValue = <T>(value: string | null | undefined, cb: (arg: string) => T) => {
|
||||
export const setKnexStringValue = <T>(
|
||||
value: string | null | undefined,
|
||||
cb: (arg: string) => T,
|
||||
allowEmptyString?: boolean
|
||||
) => {
|
||||
if (typeof value === "undefined") return;
|
||||
if (value === "" || value === null) return null;
|
||||
if ((value === "" && !allowEmptyString) || value === null) return null;
|
||||
return cb(value);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,11 @@ import { QueueWorkerProfile } from "@app/lib/types";
|
||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { ExternalPlatforms } from "@app/services/external-migration/external-migration-types";
|
||||
import { TCreateUserNotificationDTO } from "@app/services/notification/notification-types";
|
||||
import {
|
||||
TQueuePkiSyncImportCertificatesByIdDTO,
|
||||
TQueuePkiSyncRemoveCertificatesByIdDTO,
|
||||
TQueuePkiSyncSyncCertificatesByIdDTO
|
||||
} from "@app/services/pki-sync/pki-sync-types";
|
||||
import {
|
||||
TFailedIntegrationSyncEmailsPayload,
|
||||
TIntegrationSyncPayload,
|
||||
@@ -46,6 +51,7 @@ export enum QueueName {
|
||||
AuditLogPrune = "audit-log-prune",
|
||||
DailyResourceCleanUp = "daily-resource-cleanup",
|
||||
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
|
||||
PkiSyncCleanup = "pki-sync-cleanup",
|
||||
PkiSubscriber = "pki-subscriber",
|
||||
TelemetryInstanceStats = "telemtry-self-hosted-stats",
|
||||
IntegrationSync = "sync-integrations",
|
||||
@@ -58,6 +64,7 @@ export enum QueueName {
|
||||
CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber
|
||||
SecretReplication = "secret-replication",
|
||||
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
|
||||
PkiSync = "pki-sync",
|
||||
ProjectV3Migration = "project-v3-migration",
|
||||
AccessTokenStatusUpdate = "access-token-status-update",
|
||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
||||
@@ -80,6 +87,7 @@ export enum QueueJobs {
|
||||
AuditLogPrune = "audit-log-prune-job",
|
||||
DailyResourceCleanUp = "daily-resource-cleanup-job",
|
||||
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
|
||||
PkiSyncCleanup = "pki-sync-cleanup-job",
|
||||
SecWebhook = "secret-webhook-trigger",
|
||||
TelemetryInstanceStats = "telemetry-self-hosted-stats",
|
||||
IntegrationSync = "secret-integration-pull",
|
||||
@@ -91,6 +99,7 @@ export enum QueueJobs {
|
||||
CaCrlRotation = "ca-crl-rotation-job",
|
||||
SecretReplication = "secret-replication",
|
||||
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
|
||||
PkiSync = "pki-sync",
|
||||
ProjectV3Migration = "project-v3-migration",
|
||||
IdentityAccessTokenStatusUpdate = "identity-access-token-status-update",
|
||||
ServiceTokenStatusUpdate = "service-token-status-update",
|
||||
@@ -99,6 +108,9 @@ export enum QueueJobs {
|
||||
SecretSyncImportSecrets = "secret-sync-import-secrets",
|
||||
SecretSyncRemoveSecrets = "secret-sync-remove-secrets",
|
||||
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
|
||||
PkiSyncSyncCertificates = "pki-sync-sync-certificates",
|
||||
PkiSyncImportCertificates = "pki-sync-import-certificates",
|
||||
PkiSyncRemoveCertificates = "pki-sync-remove-certificates",
|
||||
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
||||
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
||||
@@ -141,6 +153,10 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.DailyExpiringPkiItemAlert;
|
||||
payload: undefined;
|
||||
};
|
||||
[QueueName.PkiSyncCleanup]: {
|
||||
name: QueueJobs.PkiSyncCleanup;
|
||||
payload: undefined;
|
||||
};
|
||||
[QueueName.AuditLogPrune]: {
|
||||
name: QueueJobs.AuditLogPrune;
|
||||
payload: undefined;
|
||||
@@ -218,6 +234,19 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.SecretSync;
|
||||
payload: TSyncSecretsDTO;
|
||||
};
|
||||
[QueueName.PkiSync]:
|
||||
| {
|
||||
name: QueueJobs.PkiSyncSyncCertificates;
|
||||
payload: TQueuePkiSyncSyncCertificatesByIdDTO;
|
||||
}
|
||||
| {
|
||||
name: QueueJobs.PkiSyncImportCertificates;
|
||||
payload: TQueuePkiSyncImportCertificatesByIdDTO;
|
||||
}
|
||||
| {
|
||||
name: QueueJobs.PkiSyncRemoveCertificates;
|
||||
payload: TQueuePkiSyncRemoveCertificatesByIdDTO;
|
||||
};
|
||||
[QueueName.ProjectV3Migration]: {
|
||||
name: QueueJobs.ProjectV3Migration;
|
||||
payload: { projectId: string };
|
||||
|
||||
@@ -43,8 +43,6 @@ export const GenericResourceNameSchema = z
|
||||
export const BaseSecretNameSchema = z.string().trim().min(1);
|
||||
|
||||
export const SecretNameSchema = BaseSecretNameSchema.refine(
|
||||
(el) => !el.includes(" "),
|
||||
"Secret name cannot contain spaces."
|
||||
)
|
||||
.refine((el) => !el.includes(":"), "Secret name cannot contain colon.")
|
||||
.refine((el) => !el.includes("/"), "Secret name cannot contain forward slash.");
|
||||
(el) => !el.includes(":"),
|
||||
"Secret name cannot contain colon."
|
||||
).refine((el) => !el.includes("/"), "Secret name cannot contain forward slash.");
|
||||
|
||||
@@ -248,6 +248,10 @@ import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-co
|
||||
import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
import { pkiSubscriberQueueServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-queue";
|
||||
import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
|
||||
import { pkiSyncCleanupQueueServiceFactory } from "@app/services/pki-sync/pki-sync-cleanup-queue";
|
||||
import { pkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
||||
import { pkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
|
||||
import { pkiSyncServiceFactory } from "@app/services/pki-sync/pki-sync-service";
|
||||
import { pkiTemplatesDALFactory } from "@app/services/pki-templates/pki-templates-dal";
|
||||
import { pkiTemplatesServiceFactory } from "@app/services/pki-templates/pki-templates-service";
|
||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||
@@ -309,6 +313,7 @@ import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry-
|
||||
import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
||||
import { totpConfigDALFactory } from "@app/services/totp/totp-config-dal";
|
||||
import { totpServiceFactory } from "@app/services/totp/totp-service";
|
||||
import { upgradePathServiceFactory } from "@app/services/upgrade-path/upgrade-path-service";
|
||||
import { userDALFactory } from "@app/services/user/user-dal";
|
||||
import { userServiceFactory } from "@app/services/user/user-service";
|
||||
import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
@@ -526,7 +531,8 @@ export const registerRoutes = async (
|
||||
orgRoleDAL,
|
||||
projectRoleDAL,
|
||||
serviceTokenDAL,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
keyStore
|
||||
});
|
||||
const assumePrivilegeService = assumePrivilegeServiceFactory({
|
||||
projectDAL,
|
||||
@@ -760,6 +766,8 @@ export const registerRoutes = async (
|
||||
userAliasDAL
|
||||
});
|
||||
|
||||
const upgradePathService = upgradePathServiceFactory({ keyStore });
|
||||
|
||||
const totpService = totpServiceFactory({
|
||||
totpConfigDAL,
|
||||
userDAL,
|
||||
@@ -781,6 +789,7 @@ export const registerRoutes = async (
|
||||
smtpService,
|
||||
authDAL,
|
||||
userDAL,
|
||||
orgMembershipDAL,
|
||||
totpConfigDAL
|
||||
});
|
||||
|
||||
@@ -979,6 +988,7 @@ export const registerRoutes = async (
|
||||
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
||||
const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db);
|
||||
const pkiSubscriberDAL = pkiSubscriberDALFactory(db);
|
||||
const pkiSyncDAL = pkiSyncDALFactory(db);
|
||||
const pkiTemplatesDAL = pkiTemplatesDALFactory(db);
|
||||
|
||||
const instanceRelayConfigDAL = instanceRelayConfigDalFactory(db);
|
||||
@@ -988,21 +998,6 @@ export const registerRoutes = async (
|
||||
|
||||
const orgGatewayConfigV2DAL = orgGatewayConfigV2DalFactory(db);
|
||||
|
||||
const certificateService = certificateServiceFactory({
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL
|
||||
});
|
||||
|
||||
const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({
|
||||
sshCertificateAuthorityDAL,
|
||||
sshCertificateAuthoritySecretDAL,
|
||||
@@ -1110,7 +1105,9 @@ export const registerRoutes = async (
|
||||
instanceRelayConfigDAL,
|
||||
orgRelayConfigDAL,
|
||||
relayDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
licenseService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const gatewayV2Service = gatewayV2ServiceFactory({
|
||||
@@ -1862,52 +1859,6 @@ export const registerRoutes = async (
|
||||
licenseService
|
||||
});
|
||||
|
||||
const certificateAuthorityQueue = certificateAuthorityQueueFactory({
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
queueService,
|
||||
pkiSubscriberDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
externalCertificateAuthorityDAL,
|
||||
keyStore,
|
||||
appConnectionDAL,
|
||||
appConnectionService
|
||||
});
|
||||
|
||||
const internalCertificateAuthorityService = internalCertificateAuthorityServiceFactory({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityQueue,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
projectDAL,
|
||||
internalCertificateAuthorityDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const certificateEstService = certificateEstServiceFactory({
|
||||
internalCertificateAuthorityService,
|
||||
certificateTemplateService,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const kmipService = kmipServiceFactory({
|
||||
kmipClientDAL,
|
||||
permissionService,
|
||||
@@ -1950,6 +1901,79 @@ export const registerRoutes = async (
|
||||
gatewayV2Service
|
||||
});
|
||||
|
||||
const pkiSyncQueue = pkiSyncQueueFactory({
|
||||
queueService,
|
||||
kmsService,
|
||||
appConnectionDAL,
|
||||
keyStore,
|
||||
pkiSyncDAL,
|
||||
auditLogService,
|
||||
projectDAL,
|
||||
licenseService,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL
|
||||
});
|
||||
|
||||
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
|
||||
queueService,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const internalCaFns = InternalCertificateAuthorityFns({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const certificateAuthorityQueue = certificateAuthorityQueueFactory({
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
queueService,
|
||||
pkiSubscriberDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
externalCertificateAuthorityDAL,
|
||||
keyStore,
|
||||
appConnectionDAL,
|
||||
appConnectionService,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const internalCertificateAuthorityService = internalCertificateAuthorityServiceFactory({
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityQueue,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
projectDAL,
|
||||
internalCertificateAuthorityDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||
certificateAuthorityDAL,
|
||||
permissionService,
|
||||
@@ -1962,19 +1986,20 @@ export const registerRoutes = async (
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
pkiSubscriberDAL,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const internalCaFns = InternalCertificateAuthorityFns({
|
||||
certificateAuthorityDAL,
|
||||
const certificateEstService = certificateEstServiceFactory({
|
||||
internalCertificateAuthorityService,
|
||||
certificateTemplateService,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
licenseService
|
||||
});
|
||||
|
||||
const pkiSubscriberQueue = pkiSubscriberQueueServiceFactory({
|
||||
@@ -1987,6 +2012,23 @@ export const registerRoutes = async (
|
||||
internalCaFns
|
||||
});
|
||||
|
||||
const certificateService = certificateServiceFactory({
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const pkiSubscriberService = pkiSubscriberServiceFactory({
|
||||
pkiSubscriberDAL,
|
||||
certificateAuthorityDAL,
|
||||
@@ -2000,7 +2042,18 @@ export const registerRoutes = async (
|
||||
kmsService,
|
||||
permissionService,
|
||||
certificateAuthorityQueue,
|
||||
internalCaFns
|
||||
internalCaFns,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const pkiSyncService = pkiSyncServiceFactory({
|
||||
pkiSyncDAL,
|
||||
pkiSubscriberDAL,
|
||||
appConnectionService,
|
||||
permissionService,
|
||||
licenseService,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const pkiTemplateService = pkiTemplatesServiceFactory({
|
||||
@@ -2065,6 +2118,7 @@ export const registerRoutes = async (
|
||||
await telemetryQueue.startTelemetryCheck();
|
||||
await telemetryQueue.startAggregatedEventsJob();
|
||||
await dailyResourceCleanUp.init();
|
||||
await pkiSyncCleanup.init();
|
||||
await dailyReminderQueueService.startDailyRemindersJob();
|
||||
await dailyReminderQueueService.startSecretReminderMigrationJob();
|
||||
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||
@@ -2148,6 +2202,7 @@ export const registerRoutes = async (
|
||||
pkiAlert: pkiAlertService,
|
||||
pkiCollection: pkiCollectionService,
|
||||
pkiSubscriber: pkiSubscriberService,
|
||||
pkiSync: pkiSyncService,
|
||||
pkiTemplate: pkiTemplateService,
|
||||
secretScanning: secretScanningService,
|
||||
license: licenseService,
|
||||
@@ -2186,7 +2241,8 @@ export const registerRoutes = async (
|
||||
reminder: reminderService,
|
||||
bus: eventBusService,
|
||||
sse: sseService,
|
||||
notification: notificationService
|
||||
notification: notificationService,
|
||||
upgradePath: upgradePathService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
||||
@@ -93,6 +93,7 @@ import {
|
||||
RailwayConnectionListItemSchema,
|
||||
SanitizedRailwayConnectionSchema
|
||||
} from "@app/services/app-connection/railway";
|
||||
import { RedisConnectionListItemSchema, SanitizedRedisConnectionSchema } from "@app/services/app-connection/redis";
|
||||
import {
|
||||
RenderConnectionListItemSchema,
|
||||
SanitizedRenderConnectionSchema
|
||||
@@ -156,7 +157,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedDigitalOceanConnectionSchema.options,
|
||||
...SanitizedNetlifyConnectionSchema.options,
|
||||
...SanitizedOktaConnectionSchema.options,
|
||||
...SanitizedAzureADCSConnectionSchema.options
|
||||
...SanitizedAzureADCSConnectionSchema.options,
|
||||
...SanitizedRedisConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -197,7 +199,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
DigitalOceanConnectionListItemSchema,
|
||||
NetlifyConnectionListItemSchema,
|
||||
OktaConnectionListItemSchema,
|
||||
AzureADCSConnectionListItemSchema
|
||||
AzureADCSConnectionListItemSchema,
|
||||
RedisConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -31,6 +31,7 @@ import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
|
||||
import { registerOktaConnectionRouter } from "./okta-connection-router";
|
||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||
import { registerRailwayConnectionRouter } from "./railway-connection-router";
|
||||
import { registerRedisConnectionRouter } from "./redis-connection-router";
|
||||
import { registerRenderConnectionRouter } from "./render-connection-router";
|
||||
import { registerSupabaseConnectionRouter } from "./supabase-connection-router";
|
||||
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
|
||||
@@ -80,5 +81,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.Supabase]: registerSupabaseConnectionRouter,
|
||||
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
|
||||
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
|
||||
[AppConnection.Okta]: registerOktaConnectionRouter
|
||||
[AppConnection.Okta]: registerOktaConnectionRouter,
|
||||
[AppConnection.Redis]: registerRedisConnectionRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateRedisConnectionSchema,
|
||||
SanitizedRedisConnectionSchema,
|
||||
UpdateRedisConnectionSchema
|
||||
} from "@app/services/app-connection/redis";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerRedisConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Redis,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedRedisConnectionSchema,
|
||||
createSchema: CreateRedisConnectionSchema,
|
||||
updateSchema: UpdateRedisConnectionSchema
|
||||
});
|
||||
};
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretFoldersSchema, SecretImportsSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { SecretFoldersSchema, SecretImportsSchema, SecretType, UsersSchema } from "@app/db/schemas";
|
||||
import { RemindersSchema } from "@app/db/schemas/reminders";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||
import { DASHBOARD } from "@app/lib/api-docs";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { secretsLimit } from "@app/server/config/rateLimiter";
|
||||
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@@ -111,6 +111,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
SecretRotationV2Schema,
|
||||
z.object({
|
||||
secrets: secretRawSchema
|
||||
.omit({ secretValue: true })
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
@@ -124,7 +125,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.array()
|
||||
.optional(),
|
||||
secrets: secretRawSchema
|
||||
.omit({ secretValue: true })
|
||||
.extend({
|
||||
isEmpty: z.boolean(),
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
@@ -219,7 +222,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImportsMultiEnv>> | undefined;
|
||||
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
|
||||
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined;
|
||||
let secrets:
|
||||
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>>[number] & { isEmpty: boolean })[]
|
||||
| undefined;
|
||||
let dynamicSecrets:
|
||||
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnvs>>
|
||||
| undefined;
|
||||
@@ -426,43 +431,51 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
||||
secrets = await server.services.secret.getSecretsRawMultiEnv({
|
||||
viewSecretValue: true,
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environments,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId,
|
||||
path: secretPath,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset,
|
||||
isInternal: true
|
||||
});
|
||||
secrets = (
|
||||
await server.services.secret.getSecretsRawMultiEnv({
|
||||
viewSecretValue: true,
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environments,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId,
|
||||
path: secretPath,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset,
|
||||
isInternal: true
|
||||
})
|
||||
).map((secret) => ({ ...secret, isEmpty: !secret.secretValue }));
|
||||
}
|
||||
}
|
||||
|
||||
if (secrets?.length || secretRotations?.length) {
|
||||
for await (const environment of environments) {
|
||||
const secretCountFromEnv =
|
||||
(secrets?.filter((secret) => secret.environment === environment).length ?? 0) +
|
||||
(secretRotations
|
||||
?.filter((rotation) => rotation.environment.slug === environment)
|
||||
.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0);
|
||||
const secretIds = [
|
||||
...new Set(
|
||||
[
|
||||
...(secrets?.filter((secret) => secret.environment === environment) ?? []),
|
||||
...(secretRotations
|
||||
?.filter((rotation) => rotation.environment.slug === environment)
|
||||
.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))) ?? [])
|
||||
].map((secret) => secret.id)
|
||||
)
|
||||
];
|
||||
|
||||
if (secretCountFromEnv) {
|
||||
if (secretIds) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
type: EventType.DASHBOARD_LIST_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secretCountFromEnv
|
||||
numberOfSecrets: secretIds.length,
|
||||
secretIds
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -473,7 +486,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secretCountFromEnv,
|
||||
numberOfSecrets: secretIds.length,
|
||||
projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
@@ -584,7 +597,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.optional(),
|
||||
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
|
||||
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
|
||||
viewSecretValue: booleanSchema.default(true),
|
||||
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
|
||||
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
|
||||
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
|
||||
@@ -606,7 +618,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
SecretRotationV2Schema,
|
||||
z.object({
|
||||
secrets: secretRawSchema
|
||||
.omit({ secretValue: true })
|
||||
.extend({
|
||||
isEmpty: z.boolean(),
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
@@ -619,7 +633,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.array()
|
||||
.optional(),
|
||||
secrets: secretRawSchema
|
||||
.omit({ secretValue: true })
|
||||
.extend({
|
||||
isEmpty: z.boolean(),
|
||||
secretReminderRecipients: z
|
||||
.object({
|
||||
user: UsersSchema.pick({ id: true, email: true, username: true }),
|
||||
@@ -715,12 +731,21 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined;
|
||||
let secrets:
|
||||
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"][number] & {
|
||||
isEmpty: boolean;
|
||||
reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null;
|
||||
})[]
|
||||
| undefined;
|
||||
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
|
||||
let secretRotations:
|
||||
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>
|
||||
| (Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>[number] & {
|
||||
secrets: (NonNullable<
|
||||
Awaited<
|
||||
ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>
|
||||
>[number]["secrets"][number] & {
|
||||
isEmpty: boolean;
|
||||
}
|
||||
> | null)[];
|
||||
})[]
|
||||
| undefined;
|
||||
|
||||
let totalImportCount: number | undefined;
|
||||
@@ -822,19 +847,31 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
);
|
||||
|
||||
if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) {
|
||||
secretRotations = await server.services.secretRotationV2.getDashboardSecretRotations(
|
||||
{
|
||||
projectId,
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
environments: [environment],
|
||||
secretPath,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
secretRotations = (
|
||||
await server.services.secretRotationV2.getDashboardSecretRotations(
|
||||
{
|
||||
projectId,
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
environments: [environment],
|
||||
secretPath,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset
|
||||
},
|
||||
req.permission
|
||||
)
|
||||
).map((rotation) => ({
|
||||
...rotation,
|
||||
secrets: rotation.secrets.map((secret) =>
|
||||
secret
|
||||
? {
|
||||
...secret,
|
||||
isEmpty: !secret.secretValue
|
||||
}
|
||||
: secret
|
||||
)
|
||||
}));
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
@@ -919,7 +956,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.secret.getSecretsRaw({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
viewSecretValue: req.query.viewSecretValue,
|
||||
viewSecretValue: true,
|
||||
throwOnMissingReadValuePermission: false,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environment,
|
||||
@@ -943,6 +980,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
secrets = rawSecrets.map((secret) => ({
|
||||
...secret,
|
||||
isEmpty: !secret.secretValue,
|
||||
reminder: reminders[secret.id] ?? null
|
||||
}));
|
||||
}
|
||||
@@ -977,19 +1015,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
}));
|
||||
|
||||
if (secrets?.length || secretRotations?.length) {
|
||||
const secretCount =
|
||||
(secrets?.length ?? 0) +
|
||||
(secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0);
|
||||
const secretIds = [
|
||||
...new Set(
|
||||
[
|
||||
...(secrets ?? []),
|
||||
...(secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))) ?? [])
|
||||
].map((secret) => secret.id)
|
||||
)
|
||||
];
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
type: EventType.DASHBOARD_LIST_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secretCount
|
||||
numberOfSecrets: secretIds.length,
|
||||
secretIds
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1000,7 +1044,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secretCount,
|
||||
numberOfSecrets: secretIds.length,
|
||||
projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
@@ -1060,6 +1104,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.array()
|
||||
.optional(),
|
||||
secrets: secretRawSchema
|
||||
.omit({ secretValue: true })
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
@@ -1145,18 +1190,20 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
);
|
||||
|
||||
for await (const environment of environments) {
|
||||
const secretCountForEnv = secrets.filter((secret) => secret.environment === environment).length;
|
||||
const envSecrets = secrets.filter((secret) => secret.environment === environment);
|
||||
const secretCountForEnv = envSecrets.length;
|
||||
|
||||
if (secretCountForEnv) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
type: EventType.DASHBOARD_LIST_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secretCountForEnv
|
||||
numberOfSecrets: secretCountForEnv,
|
||||
secretIds: envSecrets.map((secret) => secret.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1259,6 +1306,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
// TODO(scott): omit secretValue here, but requires refactor of uploading env/copy from board
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretPath: z.string().optional(),
|
||||
@@ -1310,6 +1358,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
// TODO(scott): omit secretValue here, but requires refactor of uploading env/copy from board
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
@@ -1345,11 +1394,12 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
type: EventType.DASHBOARD_LIST_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secrets.length
|
||||
numberOfSecrets: secrets.length,
|
||||
secretIds: secrets.map((secret) => secret.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1373,4 +1423,256 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
return { secrets };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/secret-value",
|
||||
config: {
|
||||
rateLimit: secretsLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||
secretKey: z.string().trim(),
|
||||
isOverride: z
|
||||
.enum(["true", "false"])
|
||||
.transform((value) => value === "true")
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
valueOverride: z.string().optional(),
|
||||
value: z.string().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { secretPath, projectId, environment, secretKey, isOverride } = req.query;
|
||||
|
||||
// TODO (scott): just get the secret instead of searching for it in list
|
||||
const { secrets } = await server.services.secret.getSecretsRaw({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
viewSecretValue: true,
|
||||
throwOnMissingReadValuePermission: false,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environment,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId,
|
||||
path: secretPath,
|
||||
search: secretKey,
|
||||
includeTagsInSearch: true,
|
||||
includeMetadataInSearch: true
|
||||
});
|
||||
|
||||
if (isOverride) {
|
||||
const personalSecret = secrets.find(
|
||||
(secret) => secret.type === SecretType.Personal && secret.secretKey === secretKey
|
||||
);
|
||||
|
||||
if (!personalSecret)
|
||||
throw new BadRequestError({
|
||||
message: `Could not find personal secret with key "${secretKey}" at secret path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
|
||||
});
|
||||
|
||||
if (personalSecret)
|
||||
return {
|
||||
valueOverride: personalSecret.secretValue
|
||||
};
|
||||
}
|
||||
|
||||
const sharedSecret = secrets.find(
|
||||
(secret) => secret.type === SecretType.Shared && secret.secretKey === secretKey
|
||||
);
|
||||
|
||||
if (!sharedSecret)
|
||||
throw new BadRequestError({
|
||||
message: `Could not find secret with key "${secretKey}" at secret path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
|
||||
});
|
||||
|
||||
// only audit if not personal
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.DASHBOARD_GET_SECRET_VALUE,
|
||||
metadata: {
|
||||
environment: req.query.environment,
|
||||
secretPath: req.query.secretPath,
|
||||
secretKey,
|
||||
secretId: sharedSecret.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { value: sharedSecret.secretValue };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/secret-imports",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: secretsLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secrets: z
|
||||
.object({
|
||||
secretPath: z.string(),
|
||||
environment: z.string(),
|
||||
environmentInfo: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
}),
|
||||
folderId: z.string().optional(),
|
||||
secrets: secretRawSchema.omit({ secretValue: true }).extend({ isEmpty: z.boolean() }).array()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const importedSecrets = await server.services.secretImport.getRawSecretsFromImports({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.query.projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.DASHBOARD_LIST_SECRETS,
|
||||
metadata: {
|
||||
environment: req.query.environment,
|
||||
secretPath: req.query.path,
|
||||
numberOfSecrets: importedSecrets.length,
|
||||
secretIds: importedSecrets.map((secret) => secret.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
secrets: importedSecrets.map((importData) => ({
|
||||
...importData,
|
||||
secrets: importData.secrets.map((secret) => ({
|
||||
...secret,
|
||||
isEmpty: !secret.secretValue
|
||||
}))
|
||||
}))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/secret-versions/:secretId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
secretId: z.string()
|
||||
}),
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number(),
|
||||
limit: z.coerce.number()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretVersions: secretRawSchema
|
||||
.omit({ secretValue: true })
|
||||
.extend({
|
||||
secretValueHidden: z.boolean()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const secretVersions = await server.services.secret.getSecretVersions({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
secretId: req.params.secretId
|
||||
});
|
||||
|
||||
return { secretVersions };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/secret-versions/:secretId/value/:version",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
secretId: z.string(),
|
||||
version: z.string()
|
||||
}),
|
||||
|
||||
response: {
|
||||
200: z.object({
|
||||
value: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { version, secretId } = req.params;
|
||||
|
||||
const [secretVersion] = await server.services.secret.getSecretVersions({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
secretId,
|
||||
secretVersions: [version]
|
||||
});
|
||||
|
||||
if (!secretVersion)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find secret version "${version}" for secret with ID "${secretId}`
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: secretVersion.workspace,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.DASHBOARD_GET_SECRET_VERSION_VALUE,
|
||||
metadata: {
|
||||
secretId,
|
||||
version
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { value: secretVersion.secretValue };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -48,6 +48,7 @@ import { registerPasswordRouter } from "./password-router";
|
||||
import { registerPkiAlertRouter } from "./pki-alert-router";
|
||||
import { registerPkiCollectionRouter } from "./pki-collection-router";
|
||||
import { registerPkiSubscriberRouter } from "./pki-subscriber-router";
|
||||
import { PKI_SYNC_REGISTER_ROUTER_MAP, registerPkiSyncRouter } from "./pki-sync-routers";
|
||||
import { registerProjectEnvRouter } from "./project-env-router";
|
||||
import { registerProjectKeyRouter } from "./project-key-router";
|
||||
import { registerProjectMembershipRouter } from "./project-membership-router";
|
||||
@@ -57,6 +58,7 @@ import { registerSecretRequestsRouter } from "./secret-requests-router";
|
||||
import { registerSecretSharingRouter } from "./secret-sharing-router";
|
||||
import { registerSecretTagRouter } from "./secret-tag-router";
|
||||
import { registerSlackRouter } from "./slack-router";
|
||||
import { registerUpgradePathRouter } from "./upgrade-path-router";
|
||||
import { registerSsoRouter } from "./sso-router";
|
||||
import { registerUserActionRouter } from "./user-action-router";
|
||||
import { registerUserEngagementRouter } from "./user-engagement-router";
|
||||
@@ -147,6 +149,15 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
|
||||
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
|
||||
await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" });
|
||||
await pkiRouter.register(
|
||||
async (pkiSyncRouter) => {
|
||||
await pkiSyncRouter.register(registerPkiSyncRouter);
|
||||
for await (const [destination, router] of Object.entries(PKI_SYNC_REGISTER_ROUTER_MAP)) {
|
||||
await pkiSyncRouter.register(router, { prefix: `/${destination}` });
|
||||
}
|
||||
},
|
||||
{ prefix: "/syncs" }
|
||||
);
|
||||
},
|
||||
{ prefix: "/pki" }
|
||||
);
|
||||
@@ -207,4 +218,5 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
);
|
||||
|
||||
await server.register(registerEventRouter, { prefix: "/events" });
|
||||
await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION,
|
||||
AzureKeyVaultPkiSyncSchema,
|
||||
CreateAzureKeyVaultPkiSyncSchema,
|
||||
UpdateAzureKeyVaultPkiSyncSchema
|
||||
} from "@app/services/pki-sync/azure-key-vault";
|
||||
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||
|
||||
import { registerSyncPkiEndpoints } from "./pki-sync-endpoints";
|
||||
|
||||
export const registerAzureKeyVaultPkiSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncPkiEndpoints({
|
||||
destination: PkiSync.AzureKeyVault,
|
||||
server,
|
||||
responseSchema: AzureKeyVaultPkiSyncSchema,
|
||||
createSchema: CreateAzureKeyVaultPkiSyncSchema,
|
||||
updateSchema: UpdateAzureKeyVaultPkiSyncSchema,
|
||||
syncOptions: {
|
||||
canImportCertificates: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION.canImportCertificates,
|
||||
canRemoveCertificates: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION.canRemoveCertificates
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||
|
||||
import { registerAzureKeyVaultPkiSyncRouter } from "./azure-key-vault-pki-sync-router";
|
||||
|
||||
export * from "./pki-sync-router";
|
||||
|
||||
export const PKI_SYNC_REGISTER_ROUTER_MAP: Record<PkiSync, (server: FastifyZodProvider) => Promise<void>> = {
|
||||
[PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter
|
||||
};
|
||||
@@ -0,0 +1,341 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||
import { PKI_SYNC_NAME_MAP } from "@app/services/pki-sync/pki-sync-maps";
|
||||
|
||||
export const registerSyncPkiEndpoints = ({
|
||||
server,
|
||||
destination,
|
||||
createSchema,
|
||||
updateSchema,
|
||||
responseSchema,
|
||||
syncOptions
|
||||
}: {
|
||||
destination: PkiSync;
|
||||
server: FastifyZodProvider;
|
||||
createSchema: z.ZodType<{
|
||||
name: string;
|
||||
projectId: string;
|
||||
connectionId: string;
|
||||
destinationConfig: Record<string, unknown>;
|
||||
syncOptions?: Record<string, unknown>;
|
||||
description?: string;
|
||||
isAutoSyncEnabled?: boolean;
|
||||
subscriberId?: string;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{
|
||||
connectionId?: string;
|
||||
name?: string;
|
||||
destinationConfig?: Record<string, unknown>;
|
||||
syncOptions?: Record<string, unknown>;
|
||||
description?: string;
|
||||
isAutoSyncEnabled?: boolean;
|
||||
subscriberId?: string;
|
||||
}>;
|
||||
responseSchema: z.ZodTypeAny;
|
||||
syncOptions: {
|
||||
canImportCertificates: boolean;
|
||||
canRemoveCertificates: boolean;
|
||||
};
|
||||
}) => {
|
||||
const destinationName = PKI_SYNC_NAME_MAP[destination];
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: `List the ${destinationName} PKI Syncs for the specified project.`,
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1, "Project ID required")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ pkiSyncs: responseSchema.array() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId }
|
||||
} = req;
|
||||
|
||||
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId }, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.GET_PKI_SYNCS,
|
||||
metadata: {
|
||||
projectId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { pkiSyncs };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:pkiSyncId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: `Get the specified ${destinationName} PKI Sync by ID.`,
|
||||
params: z.object({
|
||||
pkiSyncId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: responseSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
|
||||
const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSync.projectId,
|
||||
event: {
|
||||
type: EventType.GET_PKI_SYNC,
|
||||
metadata: {
|
||||
syncId: pkiSyncId,
|
||||
destination
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pkiSync;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: `Create a ${destinationName} PKI Sync for the specified project.`,
|
||||
body: createSchema,
|
||||
response: {
|
||||
200: responseSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const pkiSync = await server.services.pkiSync.createPkiSync({ ...req.body, destination }, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSync.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_PKI_SYNC,
|
||||
metadata: {
|
||||
pkiSyncId: pkiSync.id,
|
||||
name: pkiSync.name,
|
||||
destination
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pkiSync;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:pkiSyncId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: `Update the specified ${destinationName} PKI Sync.`,
|
||||
params: z.object({
|
||||
pkiSyncId: z.string()
|
||||
}),
|
||||
body: updateSchema,
|
||||
response: {
|
||||
200: responseSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
|
||||
const pkiSync = await server.services.pkiSync.updatePkiSync({ ...req.body, id: pkiSyncId }, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSync.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PKI_SYNC,
|
||||
metadata: {
|
||||
pkiSyncId,
|
||||
name: pkiSync.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pkiSync;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: `/:pkiSyncId`,
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: `Delete the specified ${destinationName} PKI Sync.`,
|
||||
params: z.object({
|
||||
pkiSyncId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: responseSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
|
||||
const pkiSync = await server.services.pkiSync.deletePkiSync({ id: pkiSyncId }, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSync.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_PKI_SYNC,
|
||||
metadata: {
|
||||
pkiSyncId,
|
||||
name: pkiSync.name,
|
||||
destination: pkiSync.destination
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pkiSync;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:pkiSyncId/sync",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: `Trigger a sync for the specified ${destinationName} PKI Sync.`,
|
||||
params: z.object({
|
||||
pkiSyncId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ message: z.string() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
|
||||
const result = await server.services.pkiSync.triggerPkiSyncSyncCertificatesById(
|
||||
{
|
||||
id: pkiSyncId
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
// Only register import route if the destination supports it
|
||||
if (syncOptions.canImportCertificates) {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:pkiSyncId/import",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: `Import certificates from the specified ${destinationName} PKI Sync destination.`,
|
||||
params: z.object({
|
||||
pkiSyncId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ message: z.string() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
|
||||
const result = await server.services.pkiSync.triggerPkiSyncImportCertificatesById(
|
||||
{
|
||||
id: pkiSyncId
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:pkiSyncId/remove-certificates",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: `Remove certificates from the specified ${destinationName} PKI Sync destination.`,
|
||||
params: z.object({
|
||||
pkiSyncId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ message: z.string() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
|
||||
const result = await server.services.pkiSync.triggerPkiSyncRemoveCertificatesById(
|
||||
{
|
||||
id: pkiSyncId
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,182 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||
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 { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||
|
||||
const PkiSyncSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
destination: z.nativeEnum(PkiSync),
|
||||
isAutoSyncEnabled: z.boolean(),
|
||||
destinationConfig: z.record(z.unknown()),
|
||||
syncOptions: z.record(z.unknown()),
|
||||
projectId: z.string().uuid(),
|
||||
subscriberId: z.string().uuid().nullable().optional(),
|
||||
connectionId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
// Sync status fields
|
||||
syncStatus: z.string().nullable().optional(),
|
||||
lastSyncJobId: z.string().nullable().optional(),
|
||||
lastSyncMessage: z.string().nullable().optional(),
|
||||
lastSyncedAt: z.date().nullable().optional(),
|
||||
// Import status fields
|
||||
importStatus: z.string().nullable().optional(),
|
||||
lastImportJobId: z.string().nullable().optional(),
|
||||
lastImportMessage: z.string().nullable().optional(),
|
||||
lastImportedAt: z.date().nullable().optional(),
|
||||
// Remove status fields
|
||||
removeStatus: z.string().nullable().optional(),
|
||||
lastRemoveJobId: z.string().nullable().optional(),
|
||||
lastRemoveMessage: z.string().nullable().optional(),
|
||||
lastRemovedAt: z.date().nullable().optional(),
|
||||
// App connection info
|
||||
appConnectionName: z.string(),
|
||||
appConnectionApp: z.string(),
|
||||
connection: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
app: z.string(),
|
||||
encryptedCredentials: z.unknown().nullable(),
|
||||
orgId: z.string().uuid(),
|
||||
projectId: z.string().uuid().nullable().optional(),
|
||||
method: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
version: z.number(),
|
||||
gatewayId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
isPlatformManagedCredentials: z.boolean().nullable().optional()
|
||||
}),
|
||||
subscriber: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.nullable()
|
||||
.optional()
|
||||
});
|
||||
|
||||
const PkiSyncOptionsSchema = z.object({
|
||||
name: z.string(),
|
||||
connection: z.nativeEnum(AppConnection),
|
||||
destination: z.nativeEnum(PkiSync),
|
||||
canImportCertificates: z.boolean(),
|
||||
canRemoveCertificates: z.boolean(),
|
||||
defaultCertificateNameSchema: z.string().optional(),
|
||||
forbiddenCharacters: z.string().optional(),
|
||||
allowedCharacterPattern: z.string().optional(),
|
||||
maxCertificateNameLength: z.number().optional(),
|
||||
minCertificateNameLength: z.number().optional()
|
||||
});
|
||||
|
||||
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/options",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: "List the available PKI Sync Options.",
|
||||
response: {
|
||||
200: z.object({
|
||||
pkiSyncOptions: PkiSyncOptionsSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: () => {
|
||||
const pkiSyncOptions = server.services.pkiSync.getPkiSyncOptions();
|
||||
return { pkiSyncOptions };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: "List all the PKI Syncs for the specified project.",
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ pkiSyncs: PkiSyncSchema.array() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId }, permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.GET_PKI_SYNCS,
|
||||
metadata: {
|
||||
projectId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { pkiSyncs };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:pkiSyncId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: "Get a PKI Sync by ID.",
|
||||
params: z.object({
|
||||
pkiSyncId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: PkiSyncSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
|
||||
const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSync.projectId,
|
||||
event: {
|
||||
type: EventType.GET_PKI_SYNC,
|
||||
metadata: {
|
||||
syncId: pkiSyncId,
|
||||
destination: pkiSync.destination
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pkiSync;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -316,7 +316,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
slug: slugSchema({ max: 36 }).describe("The slug of the project to get.")
|
||||
slug: slugSchema({ max: 64 }).describe("The slug of the project to get.")
|
||||
}),
|
||||
response: {
|
||||
200: projectWithEnv
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { publicEndpointLimit } from "@app/server/config/rateLimiter";
|
||||
import { versionSchema } from "@app/services/upgrade-path/upgrade-path-schemas";
|
||||
|
||||
export const registerUpgradePathRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/versions",
|
||||
config: {
|
||||
rateLimit: publicEndpointLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
versions: z.array(
|
||||
z.object({
|
||||
tagName: z.string(),
|
||||
name: z.string(),
|
||||
publishedAt: z.string(),
|
||||
prerelease: z.boolean(),
|
||||
draft: z.boolean()
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
try {
|
||||
const versions = await req.server.services.upgradePath.getGitHubReleases();
|
||||
|
||||
return {
|
||||
versions
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to fetch versions");
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new BadRequestError({ message: "Invalid query parameters" });
|
||||
}
|
||||
throw new BadRequestError({ message: "Failed to fetch GitHub releases" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/calculate",
|
||||
config: {
|
||||
rateLimit: publicEndpointLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
fromVersion: versionSchema,
|
||||
toVersion: versionSchema
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
path: z.array(
|
||||
z.object({
|
||||
version: z.string(),
|
||||
name: z.string(),
|
||||
publishedAt: z.string(),
|
||||
prerelease: z.boolean()
|
||||
})
|
||||
),
|
||||
breakingChanges: z.array(
|
||||
z.object({
|
||||
version: z.string(),
|
||||
changes: z.array(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
action: z.string()
|
||||
})
|
||||
)
|
||||
})
|
||||
),
|
||||
features: z.array(
|
||||
z.object({
|
||||
version: z.string(),
|
||||
name: z.string(),
|
||||
body: z.string(),
|
||||
publishedAt: z.string()
|
||||
})
|
||||
),
|
||||
hasDbMigration: z.boolean(),
|
||||
config: z.record(z.unknown())
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
try {
|
||||
const { fromVersion, toVersion } = req.body;
|
||||
|
||||
const result = await req.server.services.upgradePath.calculateUpgradePath(fromVersion, toVersion);
|
||||
|
||||
logger.info(
|
||||
{ pathLength: result.path.length, hasBreaking: result.breakingChanges.length > 0 },
|
||||
"Upgrade path calculated"
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to calculate upgrade path");
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new BadRequestError({ message: `Invalid input: ${error.errors.map((e) => e.message).join(", ")}` });
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
throw new BadRequestError({ message: error.message });
|
||||
}
|
||||
throw new BadRequestError({ message: "Failed to calculate upgrade path" });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -255,7 +255,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
totp: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({})
|
||||
200: z.object({
|
||||
recoveryCodes: z.string().array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT], {
|
||||
|
||||
@@ -180,7 +180,7 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to delete.")
|
||||
slug: slugSchema({ min: 5, max: 64 }).describe("The slug of the project to delete.")
|
||||
}),
|
||||
response: {
|
||||
200: SanitizedProjectSchema
|
||||
@@ -233,7 +233,7 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
slug: slugSchema({ max: 36 }).describe("The slug of the project to get.")
|
||||
slug: slugSchema({ max: 64 }).describe("The slug of the project to get.")
|
||||
}),
|
||||
response: {
|
||||
200: projectWithEnv
|
||||
@@ -266,7 +266,7 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to update.")
|
||||
slug: slugSchema({ min: 5, max: 64 }).describe("The slug of the project to update.")
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
|
||||
@@ -322,7 +322,7 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificateAuthorities],
|
||||
params: z.object({
|
||||
slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CAS.slug)
|
||||
slug: slugSchema({ min: 5, max: 64 }).describe(PROJECTS.LIST_CAS.slug)
|
||||
}),
|
||||
querystring: z.object({
|
||||
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional().describe(PROJECTS.LIST_CAS.status),
|
||||
@@ -365,7 +365,7 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
params: z.object({
|
||||
slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CERTIFICATES.slug)
|
||||
slug: slugSchema({ min: 5, max: 64 }).describe(PROJECTS.LIST_CERTIFICATES.slug)
|
||||
}),
|
||||
querystring: z.object({
|
||||
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TUsers } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -7,11 +9,54 @@ import { mfaRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
|
||||
import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
const handleMfaVerification = async (
|
||||
req: FastifyRequest & { mfa: { userId: string; orgId?: string; user: TUsers } },
|
||||
res: FastifyReply,
|
||||
server: FastifyZodProvider,
|
||||
mfaToken: string,
|
||||
mfaMethod: MfaMethod,
|
||||
isRecoveryCode?: boolean
|
||||
) => {
|
||||
const userAgent = req.headers["user-agent"];
|
||||
const mfaJwtToken = req.headers.authorization?.replace("Bearer ", "");
|
||||
if (!userAgent) throw new Error("user agent header is required");
|
||||
if (!mfaJwtToken) throw new Error("authorization header is required");
|
||||
const appCfg = getConfig();
|
||||
|
||||
const { user, token } = await server.services.login.verifyMfaToken({
|
||||
userAgent,
|
||||
mfaJwtToken,
|
||||
ip: req.realIp,
|
||||
userId: req.mfa.userId,
|
||||
orgId: req.mfa.orgId,
|
||||
mfaToken,
|
||||
mfaMethod,
|
||||
isRecoveryCode
|
||||
});
|
||||
|
||||
void res.setCookie("jid", token.refresh, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: appCfg.HTTPS_ENABLED
|
||||
});
|
||||
|
||||
addAuthOriginDomainCookie(res);
|
||||
|
||||
return {
|
||||
...user,
|
||||
token: token.access,
|
||||
protectedKey: user.protectedKey || null,
|
||||
protectedKeyIV: user.protectedKeyIV || null,
|
||||
protectedKeyTag: user.protectedKeyTag || null
|
||||
};
|
||||
};
|
||||
|
||||
export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||
const cfg = getConfig();
|
||||
|
||||
server.decorateRequest("mfa", null);
|
||||
server.addHook("preParsing", async (req, res) => {
|
||||
server.addHook("preValidation", async (req, res) => {
|
||||
const authorizationHeader = req.headers.authorization;
|
||||
|
||||
if (!authorizationHeader || !authorizationHeader.startsWith("Bearer ")) {
|
||||
@@ -109,38 +154,36 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const userAgent = req.headers["user-agent"];
|
||||
const mfaJwtToken = req.headers.authorization?.replace("Bearer ", "");
|
||||
if (!userAgent) throw new Error("user agent header is required");
|
||||
if (!mfaJwtToken) throw new Error("authorization header is required");
|
||||
const appCfg = getConfig();
|
||||
return handleMfaVerification(req, res, server, req.body.mfaToken, req.body.mfaMethod);
|
||||
}
|
||||
});
|
||||
|
||||
const { user, token } = await server.services.login.verifyMfaToken({
|
||||
userAgent,
|
||||
mfaJwtToken,
|
||||
ip: req.realIp,
|
||||
userId: req.mfa.userId,
|
||||
orgId: req.mfa.orgId,
|
||||
mfaToken: req.body.mfaToken,
|
||||
mfaMethod: req.body.mfaMethod
|
||||
});
|
||||
|
||||
void res.setCookie("jid", token.refresh, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: appCfg.HTTPS_ENABLED
|
||||
});
|
||||
|
||||
addAuthOriginDomainCookie(res);
|
||||
|
||||
return {
|
||||
...user,
|
||||
token: token.access,
|
||||
protectedKey: user.protectedKey || null,
|
||||
protectedKeyIV: user.protectedKeyIV || null,
|
||||
protectedKeyTag: user.protectedKeyTag || null
|
||||
};
|
||||
server.route({
|
||||
url: "/mfa/verify/recovery-code",
|
||||
method: "POST",
|
||||
config: {
|
||||
rateLimit: mfaRateLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
recoveryCode: z.string().trim().length(8, "Recovery code must be 8 characters")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
encryptionVersion: z.number().default(1).nullable().optional(),
|
||||
protectedKey: z.string().nullish(),
|
||||
protectedKeyIV: z.string().nullish(),
|
||||
protectedKeyTag: z.string().nullish(),
|
||||
publicKey: z.string().nullish(),
|
||||
encryptedPrivateKey: z.string().nullish(),
|
||||
iv: z.string().nullish(),
|
||||
tag: z.string().nullish(),
|
||||
token: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
return handleMfaVerification(req, res, server, req.body.recoveryCode, MfaMethod.TOTP, true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -627,7 +627,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
|
||||
secretId: secret.id,
|
||||
secretKey: req.params.secretName,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: req.body.secretMetadata
|
||||
secretMetadata: req.body.secretMetadata,
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -780,7 +781,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
|
||||
secretId: secret.id,
|
||||
secretKey: req.params.secretName,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: req.body.secretMetadata
|
||||
secretMetadata: req.body.secretMetadata,
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2154,7 +2156,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
|
||||
secretId: secret.id,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey),
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -2288,7 +2291,6 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
|
||||
return { approval: secretOperation.approval };
|
||||
}
|
||||
const { secrets } = secretOperation;
|
||||
|
||||
const secretMetadataMap = new Map(
|
||||
inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata])
|
||||
);
|
||||
@@ -2308,7 +2310,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
|
||||
secretPath: secret.secretPath,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey),
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -2328,7 +2331,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
|
||||
secretPath: secret.secretPath,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey),
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +478,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretId: secret.id,
|
||||
secretKey: req.params.secretName,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: req.body.secretMetadata
|
||||
secretMetadata: req.body.secretMetadata,
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -621,7 +622,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretId: secret.id,
|
||||
secretKey: req.params.secretName,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: req.body.secretMetadata
|
||||
secretMetadata: req.body.secretMetadata,
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -762,7 +764,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.Secrets],
|
||||
body: z.object({
|
||||
projectSlug: z.string().trim(),
|
||||
projectId: z.string().trim(),
|
||||
sourceEnvironment: z.string().trim(),
|
||||
sourceSecretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||
destinationEnvironment: z.string().trim(),
|
||||
@@ -911,7 +913,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretId: secret.id,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey),
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1063,7 +1066,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretPath: secret.secretPath,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey),
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1262,7 +1266,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const { secretName } = req.params;
|
||||
const { secretPath, environment, projectId } = req.query;
|
||||
const { tree, value } = await server.services.secret.getSecretReferenceTree({
|
||||
const { tree, value, secret } = await server.services.secret.getSecretReferenceTree({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -1273,6 +1277,21 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
environment
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRET,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
secretId: secret.id,
|
||||
secretKey: secretName,
|
||||
secretVersion: secret.version
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { tree, value };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -36,7 +36,8 @@ export enum AppConnection {
|
||||
Supabase = "supabase",
|
||||
DigitalOcean = "digital-ocean",
|
||||
Netlify = "netlify",
|
||||
Okta = "okta"
|
||||
Okta = "okta",
|
||||
Redis = "redis"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -111,6 +111,7 @@ import { getNetlifyConnectionListItem, validateNetlifyConnectionCredentials } fr
|
||||
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
|
||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
|
||||
import { getRedisConnectionListItem, RedisConnectionMethod, validateRedisConnectionCredentials } from "./redis";
|
||||
import { RenderConnectionMethod } from "./render/render-connection-enums";
|
||||
import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns";
|
||||
import {
|
||||
@@ -150,7 +151,12 @@ const SECRET_SCANNING_APP_CONNECTION_MAP = Object.fromEntries(
|
||||
);
|
||||
|
||||
// scott: ideally this would be derived from a utilized map like the above
|
||||
const PKI_APP_CONNECTIONS = [AppConnection.AWS, AppConnection.Cloudflare, AppConnection.AzureADCS];
|
||||
const PKI_APP_CONNECTIONS = [
|
||||
AppConnection.AWS,
|
||||
AppConnection.Cloudflare,
|
||||
AppConnection.AzureADCS,
|
||||
AppConnection.AzureKeyVault
|
||||
];
|
||||
|
||||
export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
return [
|
||||
@@ -191,7 +197,8 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
getSupabaseConnectionListItem(),
|
||||
getDigitalOceanConnectionListItem(),
|
||||
getNetlifyConnectionListItem(),
|
||||
getOktaConnectionListItem()
|
||||
getOktaConnectionListItem(),
|
||||
getRedisConnectionListItem()
|
||||
]
|
||||
.filter((option) => {
|
||||
switch (projectType) {
|
||||
@@ -317,7 +324,8 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service);
|
||||
@@ -364,6 +372,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case MySqlConnectionMethod.UsernameAndPassword:
|
||||
case OracleDBConnectionMethod.UsernameAndPassword:
|
||||
case AzureADCSConnectionMethod.UsernamePassword:
|
||||
case RedisConnectionMethod.UsernameAndPassword:
|
||||
return "Username & Password";
|
||||
case WindmillConnectionMethod.AccessToken:
|
||||
case HCVaultConnectionMethod.AccessToken:
|
||||
@@ -451,7 +460,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Supabase]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Netlify]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Okta]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.Okta]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Redis]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
export const enterpriseAppCheck = async (
|
||||
|
||||
@@ -38,7 +38,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Supabase]: "Supabase",
|
||||
[AppConnection.DigitalOcean]: "DigitalOcean App Platform",
|
||||
[AppConnection.Netlify]: "Netlify",
|
||||
[AppConnection.Okta]: "Okta"
|
||||
[AppConnection.Okta]: "Okta",
|
||||
[AppConnection.Redis]: "Redis"
|
||||
};
|
||||
|
||||
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
|
||||
@@ -79,5 +80,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Supabase]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.DigitalOcean]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Okta]: AppConnectionPlanType.Regular
|
||||
[AppConnection.Okta]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Redis]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -99,6 +99,7 @@ import { oktaConnectionService } from "./okta/okta-connection-service";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
import { ValidateRailwayConnectionCredentialsSchema } from "./railway";
|
||||
import { railwayConnectionService } from "./railway/railway-connection-service";
|
||||
import { ValidateRedisConnectionCredentialsSchema } from "./redis";
|
||||
import { ValidateRenderConnectionCredentialsSchema } from "./render/render-connection-schema";
|
||||
import { renderConnectionService } from "./render/render-connection-service";
|
||||
import { ValidateSupabaseConnectionCredentialsSchema } from "./supabase";
|
||||
@@ -166,7 +167,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
|
||||
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
|
||||
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema
|
||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
|
||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -266,9 +268,9 @@ export const appConnectionServiceFactory = ({
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
@@ -316,9 +318,9 @@ export const appConnectionServiceFactory = ({
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
@@ -475,9 +477,9 @@ export const appConnectionServiceFactory = ({
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
if (appConnection.projectId) {
|
||||
@@ -633,9 +635,9 @@ export const appConnectionServiceFactory = ({
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
@@ -803,9 +805,9 @@ export const appConnectionServiceFactory = ({
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
|
||||
@@ -179,6 +179,12 @@ import {
|
||||
TRailwayConnectionInput,
|
||||
TValidateRailwayConnectionCredentialsSchema
|
||||
} from "./railway";
|
||||
import {
|
||||
TRedisConnection,
|
||||
TRedisConnectionConfig,
|
||||
TRedisConnectionInput,
|
||||
TValidateRedisConnectionCredentialsSchema
|
||||
} from "./redis";
|
||||
import {
|
||||
TRenderConnection,
|
||||
TRenderConnectionConfig,
|
||||
@@ -261,6 +267,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TDigitalOceanConnection
|
||||
| TNetlifyConnection
|
||||
| TOktaConnection
|
||||
| TRedisConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -306,6 +313,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TDigitalOceanConnectionInput
|
||||
| TNetlifyConnectionInput
|
||||
| TOktaConnectionInput
|
||||
| TRedisConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput =
|
||||
@@ -368,7 +376,8 @@ export type TAppConnectionConfig =
|
||||
| TSupabaseConnectionConfig
|
||||
| TDigitalOceanConnectionConfig
|
||||
| TNetlifyConnectionConfig
|
||||
| TOktaConnectionConfig;
|
||||
| TOktaConnectionConfig
|
||||
| TRedisConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -408,7 +417,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateSupabaseConnectionCredentialsSchema
|
||||
| TValidateDigitalOceanCredentialsSchema
|
||||
| TValidateNetlifyConnectionCredentialsSchema
|
||||
| TValidateOktaConnectionCredentialsSchema;
|
||||
| TValidateOktaConnectionCredentialsSchema
|
||||
| TValidateRedisConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
|
||||
@@ -22,13 +22,13 @@ interface HerokuOAuthTokenResponse {
|
||||
}
|
||||
|
||||
export const getHerokuConnectionListItem = () => {
|
||||
const { CLIENT_ID_HEROKU } = getConfig();
|
||||
const { INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID } = getConfig();
|
||||
|
||||
return {
|
||||
name: "Heroku" as const,
|
||||
app: AppConnection.Heroku as const,
|
||||
methods: Object.values(HerokuConnectionMethod) as [HerokuConnectionMethod.AuthToken, HerokuConnectionMethod.OAuth],
|
||||
oauthClientId: CLIENT_ID_HEROKU
|
||||
oauthClientId: INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID
|
||||
};
|
||||
};
|
||||
|
||||
@@ -40,12 +40,12 @@ export const refreshHerokuToken = async (
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
): Promise<string> => {
|
||||
const { CLIENT_SECRET_HEROKU } = getConfig();
|
||||
const { INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET } = getConfig();
|
||||
|
||||
const payload = {
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_secret: CLIENT_SECRET_HEROKU
|
||||
client_secret: INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET
|
||||
};
|
||||
|
||||
const { data } = await request.post<{ access_token: string; expires_in: number }>(
|
||||
@@ -75,7 +75,7 @@ export const refreshHerokuToken = async (
|
||||
};
|
||||
|
||||
export const exchangeHerokuOAuthCode = async (code: string): Promise<HerokuOAuthTokenResponse> => {
|
||||
const { CLIENT_SECRET_HEROKU } = getConfig();
|
||||
const { INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET } = getConfig();
|
||||
|
||||
try {
|
||||
const response = await request.post<HerokuOAuthTokenResponse>(
|
||||
@@ -83,7 +83,7 @@ export const exchangeHerokuOAuthCode = async (code: string): Promise<HerokuOAuth
|
||||
{
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_secret: CLIENT_SECRET_HEROKU
|
||||
client_secret: INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./redis-connection-enums";
|
||||
export * from "./redis-connection-fns";
|
||||
export * from "./redis-connection-schemas";
|
||||
export * from "./redis-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum RedisConnectionMethod {
|
||||
UsernameAndPassword = "username-and-password"
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import Redis from "ioredis";
|
||||
|
||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { RedisConnectionMethod } from "./redis-connection-enums";
|
||||
import { TRedisConnectionConfig } from "./redis-connection-types";
|
||||
|
||||
export const getRedisConnectionListItem = () => {
|
||||
return {
|
||||
name: "Redis" as const,
|
||||
app: AppConnection.Redis as const,
|
||||
methods: Object.values(RedisConnectionMethod) as [RedisConnectionMethod.UsernameAndPassword],
|
||||
supportsPlatformManagement: false as const
|
||||
};
|
||||
};
|
||||
|
||||
export const validateRedisConnectionCredentials = async (config: TRedisConnectionConfig) => {
|
||||
const [hostIp] = await verifyHostInputValidity(config.credentials.host);
|
||||
|
||||
let connection: Redis | null = null;
|
||||
try {
|
||||
connection = new Redis({
|
||||
username: config.credentials.username,
|
||||
host: hostIp,
|
||||
port: config.credentials.port,
|
||||
password: config.credentials.password,
|
||||
...(config.credentials.sslEnabled && {
|
||||
tls: {
|
||||
rejectUnauthorized: config.credentials.sslRejectUnauthorized,
|
||||
ca: config.credentials.sslCertificate
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let result: string;
|
||||
if (config.credentials.password) {
|
||||
result = await connection.auth(config.credentials.username, config.credentials.password, () => {});
|
||||
} else {
|
||||
result = await connection.auth(config.credentials.username, () => {});
|
||||
}
|
||||
|
||||
if (result !== "OK") {
|
||||
throw new BadRequestError({ message: `Invalid credentials, Redis returned ${result} status` });
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestError) {
|
||||
throw err;
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection: ${(err as Error)?.message || "verify credentials"}`
|
||||
});
|
||||
} finally {
|
||||
if (connection) await connection.quit();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { RedisConnectionMethod } from "./redis-connection-enums";
|
||||
|
||||
export const BaseRedisUsernameAndPasswordConnectionSchema = z.object({
|
||||
host: z.string().toLowerCase().min(1),
|
||||
port: z.coerce.number(),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1).optional(),
|
||||
|
||||
sslRejectUnauthorized: z.boolean(),
|
||||
sslEnabled: z.boolean(),
|
||||
sslCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value) => value || undefined)
|
||||
.optional()
|
||||
});
|
||||
|
||||
export const RedisConnectionAccessTokenCredentialsSchema = BaseRedisUsernameAndPasswordConnectionSchema;
|
||||
|
||||
const BaseRedisConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Redis) });
|
||||
|
||||
export const RedisConnectionSchema = BaseRedisConnectionSchema.extend({
|
||||
method: z.literal(RedisConnectionMethod.UsernameAndPassword),
|
||||
credentials: RedisConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedRedisConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseRedisConnectionSchema.extend({
|
||||
method: z.literal(RedisConnectionMethod.UsernameAndPassword),
|
||||
credentials: RedisConnectionAccessTokenCredentialsSchema.pick({
|
||||
host: true,
|
||||
port: true,
|
||||
username: true,
|
||||
sslEnabled: true,
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateRedisConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(RedisConnectionMethod.UsernameAndPassword)
|
||||
.describe(AppConnections.CREATE(AppConnection.Redis).method),
|
||||
credentials: RedisConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Redis).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateRedisConnectionSchema = ValidateRedisConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Redis, {
|
||||
supportsPlatformManagedCredentials: false,
|
||||
supportsGateways: false
|
||||
})
|
||||
);
|
||||
|
||||
export const UpdateRedisConnectionSchema = z
|
||||
.object({
|
||||
credentials: RedisConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.Redis).credentials
|
||||
)
|
||||
})
|
||||
.and(
|
||||
GenericUpdateAppConnectionFieldsSchema(AppConnection.Redis, {
|
||||
supportsPlatformManagedCredentials: false,
|
||||
supportsGateways: false
|
||||
})
|
||||
);
|
||||
|
||||
export const RedisConnectionListItemSchema = z.object({
|
||||
name: z.literal("Redis"),
|
||||
app: z.literal(AppConnection.Redis),
|
||||
methods: z.nativeEnum(RedisConnectionMethod).array(),
|
||||
supportsPlatformManagement: z.literal(false)
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateRedisConnectionSchema,
|
||||
RedisConnectionSchema,
|
||||
ValidateRedisConnectionCredentialsSchema
|
||||
} from "./redis-connection-schemas";
|
||||
|
||||
export type TRedisConnection = z.infer<typeof RedisConnectionSchema>;
|
||||
|
||||
export type TRedisConnectionInput = z.infer<typeof CreateRedisConnectionSchema> & {
|
||||
app: AppConnection.Redis;
|
||||
};
|
||||
|
||||
export type TValidateRedisConnectionCredentialsSchema = typeof ValidateRedisConnectionCredentialsSchema;
|
||||
|
||||
export type TRedisConnectionConfig = DiscriminativePick<TRedisConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
@@ -684,7 +684,8 @@ export const authLoginServiceFactory = ({
|
||||
mfaJwtToken,
|
||||
ip,
|
||||
userAgent,
|
||||
orgId
|
||||
orgId,
|
||||
isRecoveryCode = false
|
||||
}: TVerifyMfaTokenDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const user = await userDAL.findById(userId);
|
||||
@@ -698,16 +699,21 @@ export const authLoginServiceFactory = ({
|
||||
code: mfaToken
|
||||
});
|
||||
} else if (mfaMethod === MfaMethod.TOTP) {
|
||||
if (mfaToken.length === 6) {
|
||||
await totpService.verifyUserTotp({
|
||||
userId,
|
||||
totp: mfaToken
|
||||
});
|
||||
} else {
|
||||
if (isRecoveryCode) {
|
||||
await totpService.verifyWithUserRecoveryCode({
|
||||
userId,
|
||||
recoveryCode: mfaToken
|
||||
});
|
||||
} else {
|
||||
if (mfaToken.length !== 6) {
|
||||
throw new BadRequestError({
|
||||
message: "Please use a valid TOTP code."
|
||||
});
|
||||
}
|
||||
await totpService.verifyUserTotp({
|
||||
userId,
|
||||
totp: mfaToken
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -24,6 +24,7 @@ export type TVerifyMfaTokenDTO = {
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
orgId?: string;
|
||||
isRecoveryCode?: boolean;
|
||||
};
|
||||
|
||||
export type TOauthLoginDTO = {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
@@ -22,6 +23,7 @@ import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
|
||||
type TAuthPasswordServiceFactoryDep = {
|
||||
authDAL: TAuthDALFactory;
|
||||
userDAL: TUserDALFactory;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">;
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
totpConfigDAL: Pick<TTotpConfigDALFactory, "delete">;
|
||||
@@ -31,6 +33,7 @@ export type TAuthPasswordFactory = ReturnType<typeof authPaswordServiceFactory>;
|
||||
export const authPaswordServiceFactory = ({
|
||||
authDAL,
|
||||
userDAL,
|
||||
orgMembershipDAL,
|
||||
tokenService,
|
||||
smtpService,
|
||||
totpConfigDAL
|
||||
@@ -47,21 +50,46 @@ export const authPaswordServiceFactory = ({
|
||||
|
||||
if (user && user.isAccepted) {
|
||||
const cfg = getConfig();
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.ResetPassword,
|
||||
recipients: [email],
|
||||
subjectLine: "Infisical password reset",
|
||||
substitutions: {
|
||||
const hasEmailAuth = user.authMethods?.includes(AuthMethod.EMAIL);
|
||||
|
||||
if (!hasEmailAuth) {
|
||||
const orgMemberships = await orgMembershipDAL.find({ userId: user.id });
|
||||
const lastLoginMethod =
|
||||
orgMemberships
|
||||
.filter((membership) => membership.lastLoginAuthMethod)
|
||||
.sort((a, b) => (b.updatedAt || new Date(0)).getTime() - (a.updatedAt || new Date(0)).getTime())[0]
|
||||
?.lastLoginAuthMethod || null;
|
||||
const substitutions = {
|
||||
email,
|
||||
token,
|
||||
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
|
||||
}
|
||||
});
|
||||
lastLoginMethod,
|
||||
isCloud: cfg.isCloud,
|
||||
siteUrl: cfg.SITE_URL || ""
|
||||
};
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OAuthPasswordReset,
|
||||
recipients: [email],
|
||||
subjectLine: "Password reset not available",
|
||||
substitutions
|
||||
});
|
||||
} else {
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.ResetPassword,
|
||||
recipients: [email],
|
||||
subjectLine: "Infisical password reset",
|
||||
substitutions: {
|
||||
email,
|
||||
token,
|
||||
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ import {
|
||||
} from "@app/services/certificate/certificate-types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
||||
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
|
||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
@@ -56,6 +59,8 @@ type TAcmeCertificateAuthorityFnsDeps = {
|
||||
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
|
||||
>;
|
||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
|
||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
|
||||
};
|
||||
|
||||
@@ -109,7 +114,9 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
projectDAL,
|
||||
pkiSubscriberDAL
|
||||
pkiSubscriberDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
}: TAcmeCertificateAuthorityFnsDeps) => {
|
||||
const createCertificateAuthority = async ({
|
||||
name,
|
||||
@@ -524,6 +531,8 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
+10
-1
@@ -26,6 +26,9 @@ import {
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
import { TPkiSubscriberProperties } from "@app/services/pki-subscriber/pki-subscriber-types";
|
||||
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
||||
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
|
||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
@@ -55,6 +58,8 @@ type TAzureAdCsCertificateAuthorityFnsDeps = {
|
||||
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
|
||||
>;
|
||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
|
||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
|
||||
};
|
||||
|
||||
@@ -584,7 +589,9 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
projectDAL,
|
||||
pkiSubscriberDAL
|
||||
pkiSubscriberDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
}: TAzureAdCsCertificateAuthorityFnsDeps) => {
|
||||
const createCertificateAuthority = async ({
|
||||
name,
|
||||
@@ -1024,6 +1031,8 @@ export const AzureAdCsCertificateAuthorityFns = ({
|
||||
);
|
||||
});
|
||||
|
||||
await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
|
||||
|
||||
return {
|
||||
certificate: certificatePem,
|
||||
certificateChain: certificateChainPem,
|
||||
|
||||
@@ -20,6 +20,8 @@ import { TCertificateBodyDALFactory } from "../certificate/certificate-body-dal"
|
||||
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
|
||||
import { SubscriberOperationStatus } from "../pki-subscriber/pki-subscriber-types";
|
||||
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
|
||||
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
|
||||
import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns";
|
||||
import { AzureAdCsCertificateAuthorityFns } from "./azure-ad-cs/azure-ad-cs-certificate-authority-fns";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
@@ -50,6 +52,8 @@ type TCertificateAuthorityQueueFactoryDep = {
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||
queueService: TQueueServiceFactory;
|
||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById" | "updateById">;
|
||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||
};
|
||||
|
||||
export type TCertificateAuthorityQueueFactory = ReturnType<typeof certificateAuthorityQueueFactory>;
|
||||
@@ -68,7 +72,9 @@ export const certificateAuthorityQueueFactory = ({
|
||||
externalCertificateAuthorityDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
pkiSubscriberDAL
|
||||
pkiSubscriberDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
}: TCertificateAuthorityQueueFactoryDep) => {
|
||||
const acmeFns = AcmeCertificateAuthorityFns({
|
||||
appConnectionDAL,
|
||||
@@ -80,7 +86,9 @@ export const certificateAuthorityQueueFactory = ({
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
pkiSubscriberDAL,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
|
||||
@@ -93,7 +101,9 @@ export const certificateAuthorityQueueFactory = ({
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
pkiSubscriberDAL,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
// TODO 1: auto-periodic rotation
|
||||
|
||||
@@ -13,6 +13,8 @@ import { TCertificateDALFactory } from "../certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
|
||||
import { TPkiSyncDALFactory } from "../pki-sync/pki-sync-dal";
|
||||
import { TPkiSyncQueueFactory } from "../pki-sync/pki-sync-queue";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import {
|
||||
AcmeCertificateAuthorityFns,
|
||||
@@ -68,6 +70,8 @@ type TCertificateAuthorityServiceFactoryDep = {
|
||||
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
|
||||
>;
|
||||
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
|
||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||
};
|
||||
|
||||
export type TCertificateAuthorityServiceFactory = ReturnType<typeof certificateAuthorityServiceFactory>;
|
||||
@@ -84,7 +88,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
pkiSubscriberDAL
|
||||
pkiSubscriberDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
}: TCertificateAuthorityServiceFactoryDep) => {
|
||||
const acmeFns = AcmeCertificateAuthorityFns({
|
||||
appConnectionDAL,
|
||||
@@ -96,7 +102,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
pkiSubscriberDAL,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
|
||||
@@ -109,7 +117,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
pkiSubscriberDAL,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const createCertificateAuthority = async (
|
||||
|
||||
+10
-1
@@ -19,6 +19,9 @@ import {
|
||||
TAltNameMapping
|
||||
} from "@app/services/certificate/certificate-types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
||||
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
|
||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
@@ -51,6 +54,8 @@ type TInternalCertificateAuthorityFnsDeps = {
|
||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||
};
|
||||
|
||||
export const InternalCertificateAuthorityFns = ({
|
||||
@@ -62,7 +67,9 @@ export const InternalCertificateAuthorityFns = ({
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL
|
||||
certificateSecretDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
}: TInternalCertificateAuthorityFnsDeps) => {
|
||||
const issueCertificate = async (
|
||||
subscriber: TPkiSubscribers,
|
||||
@@ -251,6 +258,8 @@ export const InternalCertificateAuthorityFns = ({
|
||||
);
|
||||
});
|
||||
|
||||
await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
|
||||
|
||||
return {
|
||||
certificate: leafCert.toString("pem"),
|
||||
certificateChain: certificateChainPem,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TableName, TCertificates } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
@@ -25,6 +25,20 @@ export const certificateDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findAllActiveCertsForSubscriber = async ({ subscriberId }: { subscriberId: string }) => {
|
||||
try {
|
||||
const certs = await db
|
||||
.replicaNode()(TableName.Certificate)
|
||||
.where({ pkiSubscriberId: subscriberId, status: CertStatus.ACTIVE })
|
||||
.where("notAfter", ">", new Date())
|
||||
.orderBy("notBefore", "desc");
|
||||
|
||||
return certs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find all active certificates for subscriber" });
|
||||
}
|
||||
};
|
||||
|
||||
const countCertificatesInProject = async ({
|
||||
projectId,
|
||||
friendlyName,
|
||||
@@ -79,10 +93,33 @@ export const certificateDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findExpiredSyncedCertificates = async (): Promise<TCertificates[]> => {
|
||||
try {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
yesterday.setHours(0, 0, 0, 0);
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const certs = await db
|
||||
.replicaNode()(TableName.Certificate)
|
||||
.where("notAfter", ">=", yesterday)
|
||||
.where("notAfter", "<", today)
|
||||
.whereNotNull("pkiSubscriberId");
|
||||
|
||||
return certs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find expired synced certificates" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...certificateOrm,
|
||||
countCertificatesInProject,
|
||||
countCertificatesForPkiSubscriber,
|
||||
findLatestActiveCertForSubscriber
|
||||
findLatestActiveCertForSubscriber,
|
||||
findAllActiveCertsForSubscriber,
|
||||
findExpiredSyncedCertificates
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
@@ -20,6 +21,9 @@ import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||
import { TPkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal";
|
||||
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
|
||||
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
|
||||
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
@@ -53,6 +57,8 @@ type TCertificateServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
|
||||
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
|
||||
};
|
||||
|
||||
export type TCertificateServiceFactory = ReturnType<typeof certificateServiceFactory>;
|
||||
@@ -69,7 +75,9 @@ export const certificateServiceFactory = ({
|
||||
pkiCollectionItemDAL,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
permissionService,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
}: TCertificateServiceFactoryDep) => {
|
||||
/**
|
||||
* Return details for certificate with serial number [serialNumber]
|
||||
@@ -158,6 +166,11 @@ export const certificateServiceFactory = ({
|
||||
|
||||
const deletedCert = await certificateDAL.deleteById(cert.id);
|
||||
|
||||
// Trigger auto sync for PKI syncs connected to this certificate's subscriber
|
||||
if (cert.pkiSubscriberId) {
|
||||
await triggerAutoSyncForSubscriber(cert.pkiSubscriberId, { pkiSyncDAL, pkiSyncQueue });
|
||||
}
|
||||
|
||||
return {
|
||||
deletedCert
|
||||
};
|
||||
@@ -222,6 +235,11 @@ export const certificateServiceFactory = ({
|
||||
}
|
||||
);
|
||||
|
||||
// Trigger auto sync for PKI syncs connected to this certificate's subscriber
|
||||
if (cert.pkiSubscriberId) {
|
||||
await triggerAutoSyncForSubscriber(cert.pkiSubscriberId, { pkiSyncDAL, pkiSyncQueue });
|
||||
}
|
||||
|
||||
// Note: External CA revocation handling would go here for supported CA types
|
||||
// Currently, only internal CAs and ACME CAs support revocation
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user