Compare commits

...

7 Commits

Author SHA1 Message Date
bcf86aea90 wip 2025-03-18 20:21:05 -07:00
06024065cd wip 2025-03-18 18:04:16 -07:00
2d207147a6 wip 2025-03-17 19:49:34 -07:00
4c04d0f871 wip 2025-03-17 19:39:23 -07:00
484a9b28ef wip 2025-03-17 19:34:43 -07:00
94f79ade4a wip 2025-03-17 19:17:39 -07:00
5a63a6dbe4 wip 2025-03-17 16:15:18 -07:00
112 changed files with 4343 additions and 218 deletions

View File

@ -33,6 +33,7 @@ import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
import { TSecretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service";
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
@ -230,6 +231,7 @@ declare module "fastify" {
kmip: TKmipServiceFactory;
kmipOperation: TKmipOperationServiceFactory;
gateway: TGatewayServiceFactory;
secretRotationV2: TSecretRotationV2ServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

View File

@ -393,6 +393,11 @@ import {
TExternalGroupOrgRoleMappingsInsert,
TExternalGroupOrgRoleMappingsUpdate
} from "@app/db/schemas/external-group-org-role-mappings";
import {
TSecretRotationsV2,
TSecretRotationsV2Insert,
TSecretRotationsV2Update
} from "@app/db/schemas/secret-rotations-v2";
import { TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate } from "@app/db/schemas/secret-syncs";
import {
TSecretV2TagJunction,
@ -950,5 +955,10 @@ declare module "knex/types/tables" {
TOrgGatewayConfigInsert,
TOrgGatewayConfigUpdate
>;
[TableName.SecretRotationV2]: KnexOriginal.CompositeTableType<
TSecretRotationsV2,
TSecretRotationsV2Insert,
TSecretRotationsV2Update
>;
}
}

View File

@ -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.hasColumn(TableName.AppConnection, "isPlatformManaged"))) {
await knex.schema.alterTable(TableName.AppConnection, (t) => {
t.boolean("isPlatformManaged").defaultTo(false);
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.AppConnection, "isPlatformManaged")) {
await knex.schema.alterTable(TableName.AppConnection, (t) => {
t.dropColumn("isPlatformManaged");
});
}
}

View File

@ -0,0 +1,44 @@
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.SecretRotationV2))) {
await knex.schema.createTable(TableName.SecretRotationV2, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name", 32).notNullable();
t.string("description");
t.string("type").notNullable();
t.integer("interval").notNullable();
t.jsonb("parameters").notNullable();
t.binary("encryptedGeneratedCredentials").notNullable();
t.boolean("isAutoRotationEnabled").notNullable().defaultTo(true);
t.integer("activeIndex").notNullable().defaultTo(0);
// we're including projectId in addition to folder ID because we allow folderId to be null (if the folder
// is deleted), to preserve configuration
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("folderId");
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("SET NULL");
t.uuid("connectionId").notNullable();
t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
t.timestamps(true, true, true);
t.string("rotationStatus");
t.string("lastRotationJobId");
t.string("lastRotationMessage", 1024);
t.datetime("lastRotatedAt");
});
await createOnUpdateTrigger(knex, TableName.SecretRotationV2);
await knex.schema.alterTable(TableName.SecretRotationV2, (t) => {
t.unique(["projectId", "name"]);
});
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.SecretRotationV2);
await dropOnUpdateTrigger(knex, TableName.SecretRotationV2);
}

View File

@ -19,7 +19,8 @@ export const AppConnectionsSchema = z.object({
version: z.number().default(1),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
isPlatformManaged: z.boolean().default(false).nullable().optional()
});
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;

View File

@ -140,7 +140,8 @@ export enum TableName {
KmipClient = "kmip_clients",
KmipOrgConfig = "kmip_org_configs",
KmipOrgServerCertificates = "kmip_org_server_certificates",
KmipClientCertificates = "kmip_client_certificates"
KmipClientCertificates = "kmip_client_certificates",
SecretRotationV2 = "secret_rotations_v2"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

View File

@ -0,0 +1,35 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SecretRotationsV2Schema = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable().optional(),
type: z.string(),
interval: z.number(),
parameters: z.unknown(),
encryptedGeneratedCredentials: zodBuffer,
isAutoRotationEnabled: z.boolean().default(true),
activeIndex: z.number().default(0),
projectId: z.string(),
folderId: z.string().uuid().nullable().optional(),
connectionId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
rotationStatus: z.string().nullable().optional(),
lastRotationJobId: z.string().nullable().optional(),
lastRotationMessage: z.string().nullable().optional(),
lastRotatedAt: z.date().nullable().optional()
});
export type TSecretRotationsV2 = z.infer<typeof SecretRotationsV2Schema>;
export type TSecretRotationsV2Insert = Omit<z.input<typeof SecretRotationsV2Schema>, TImmutableDBKeys>;
export type TSecretRotationsV2Update = Partial<Omit<z.input<typeof SecretRotationsV2Schema>, TImmutableDBKeys>>;

View File

@ -12,7 +12,6 @@ import { TImmutableDBKeys } from "./models";
export const SecretSharingSchema = z.object({
id: z.string().uuid(),
encryptedValue: z.string().nullable().optional(),
type: z.string(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
hashedHex: z.string().nullable().optional(),
@ -27,7 +26,8 @@ export const SecretSharingSchema = z.object({
lastViewedAt: z.date().nullable().optional(),
password: z.string().nullable().optional(),
encryptedSecret: zodBuffer.nullable().optional(),
identifier: z.string().nullable().optional()
identifier: z.string().nullable().optional(),
type: z.string().default("share")
});
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;

View File

@ -1,3 +1,8 @@
import {
registerSecretRotationV2Router,
SECRET_ROTATION_REGISTER_ROUTER_MAP
} from "@app/ee/routes/v2/secret-rotation-v2-routers";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerProjectRoleRouter } from "./project-role-router";
@ -13,4 +18,17 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerIdentityProjectAdditionalPrivilegeRouter, {
prefix: "/identity-project-additional-privilege"
});
await server.register(
async (secretRotationV2Router) => {
// register generic secret sync endpoints
await secretRotationV2Router.register(registerSecretRotationV2Router);
// register service specific secret rotation endpoints (secret-rotations/postgres-credentials, etc.)
for await (const [type, router] of Object.entries(SECRET_ROTATION_REGISTER_ROUTER_MAP)) {
await secretRotationV2Router.register(router, { prefix: `/${type}` });
}
},
{ prefix: "/secret-rotations" }
);
};

View File

@ -0,0 +1,12 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
export * from "./secret-rotation-v2-router";
export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
SecretRotation,
(server: FastifyZodProvider) => Promise<void>
> = {
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter
};

View File

@ -0,0 +1,17 @@
import {
CreatePostgresCredentialsRotationSchema,
PostgresCredentialsRotationSchema,
UpdatePostgresCredentialsRotationSchema
} from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
export const registerPostgresCredentialsRotationRouter = async (server: FastifyZodProvider) =>
registerSecretRotationEndpoints({
type: SecretRotation.PostgresCredentials,
server,
responseSchema: PostgresCredentialsRotationSchema,
createSchema: CreatePostgresCredentialsRotationSchema,
updateSchema: UpdatePostgresCredentialsRotationSchema
});

View File

@ -0,0 +1,402 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { SECRET_ROTATION_NAME_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
import {
TSecretRotationV2,
TSecretRotationV2Input
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { SecretRotations } from "@app/lib/api-docs";
import { startsWithVowel } from "@app/lib/fn";
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";
export const registerSecretRotationEndpoints = <T extends TSecretRotationV2, I extends TSecretRotationV2Input>({
server,
type,
createSchema,
updateSchema,
responseSchema
}: {
type: SecretRotation;
server: FastifyZodProvider;
createSchema: z.ZodType<{
name: string;
environment: string;
secretPath: string;
projectId: string;
connectionId: string;
parameters: I["parameters"];
description?: string | null;
isAutoRotationEnabled?: boolean;
interval: number;
}>;
updateSchema: z.ZodType<{
connectionId?: string;
name?: string;
environment?: string;
secretPath?: string;
parameters?: I["parameters"];
description?: string | null;
isAutoRotationEnabled?: boolean;
interval?: number;
}>;
responseSchema: z.ZodTypeAny;
}) => {
const rotationType = SECRET_ROTATION_NAME_MAP[type];
server.route({
method: "GET",
url: `/`,
config: {
rateLimit: readLimit
},
schema: {
description: `List the ${rotationType} Rotations for the specified project.`,
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.LIST(type).projectId)
}),
response: {
200: z.object({ secretRotations: responseSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId }
} = req;
const secretRotations = (await server.services.secretRotationV2.listSecretRotationsByProjectId(
{ projectId, type },
req.permission
)) as T[];
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_SECRET_ROTATIONS,
metadata: {
type,
count: secretRotations.length,
rotationIds: secretRotations.map((rotation) => rotation.id)
}
}
});
return { secretRotations };
}
});
server.route({
method: "GET",
url: "/:rotationId",
config: {
rateLimit: readLimit
},
schema: {
description: `Get the specified ${rotationType} Rotation by ID.`,
params: z.object({
rotationId: z.string().uuid().describe(SecretRotations.GET_BY_ID(type).rotationId)
}),
response: {
200: z.object({ secretRotation: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { rotationId } = req.params;
const secretRotation = (await server.services.secretRotationV2.findSecretRotationById(
{ rotationId, type },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: secretRotation.projectId,
event: {
type: EventType.GET_SECRET_ROTATION,
metadata: {
rotationId,
type
}
}
});
return { secretRotation };
}
});
server.route({
method: "GET",
url: `/rotation-name/:rotationName`,
config: {
rateLimit: readLimit
},
schema: {
description: `Get the specified ${rotationType} Rotation by name and project ID.`,
params: z.object({
rotationName: z
.string()
.trim()
.min(1, "Rotation name required")
.describe(SecretRotations.GET_BY_NAME(type).rotationName)
}),
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.GET_BY_NAME(type).projectId)
}),
response: {
200: z.object({ secretRotation: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { rotationName } = req.params;
const { projectId } = req.query;
const secretRotation = (await server.services.secretRotationV2.findSecretRotationByName(
{ rotationName, projectId, type },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_SECRET_ROTATION,
metadata: {
rotationId: secretRotation.id,
type
}
}
});
return { secretRotation };
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
description: `Create ${
startsWithVowel(rotationType) ? "an" : "a"
} ${rotationType} Rotation for the specified project.`,
body: createSchema,
response: {
200: z.object({ secretRotation: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const secretRotation = (await server.services.secretRotationV2.createSecretRotation(
{ ...req.body, type },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: secretRotation.projectId,
event: {
type: EventType.CREATE_SECRET_ROTATION,
metadata: {
rotationId: secretRotation.id,
type,
...req.body
}
}
});
return { secretRotation };
}
});
server.route({
method: "PATCH",
url: "/:rotationId",
config: {
rateLimit: writeLimit
},
schema: {
description: `Update the specified ${rotationType} Rotation.`,
params: z.object({
rotationId: z.string().uuid().describe(SecretRotations.UPDATE(type).rotationId)
}),
body: updateSchema,
response: {
200: z.object({ secretRotation: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { rotationId } = req.params;
const secretRotation = (await server.services.secretRotationV2.updateSecretRotation(
{ ...req.body, rotationId, type },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: secretRotation.projectId,
event: {
type: EventType.UPDATE_SECRET_ROTATION,
metadata: {
rotationId,
type,
...req.body
}
}
});
return { secretRotation };
}
});
server.route({
method: "DELETE",
url: `/:rotationId`,
config: {
rateLimit: writeLimit
},
schema: {
description: `Delete the specified ${rotationType} Rotation.`,
params: z.object({
rotationId: z.string().uuid().describe(SecretRotations.DELETE(type).rotationId)
}),
querystring: z.object({
removeSecrets: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(SecretRotations.DELETE(type).removeSecrets)
}),
response: {
200: z.object({ secretRotation: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { rotationId } = req.params;
const { removeSecrets } = req.query;
const secretRotation = (await server.services.secretRotationV2.deleteSecretRotation(
{ type, rotationId, removeSecrets },
req.permission
)) as T;
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_SECRET_ROTATION,
metadata: {
type,
rotationId,
removeSecrets
}
}
});
return { secretRotation };
}
});
server.route({
method: "GET",
url: "/:rotationId/credentials",
config: {
rateLimit: readLimit
},
schema: {
description: `Get the active and inactive credentials for the specified ${rotationType} Rotation.`,
params: z.object({
rotationId: z.string().uuid().describe(SecretRotations.GET_CREDENTIALS_BY_ID(type).rotationId)
}),
response: {
200: z.object({ secretRotation: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { rotationId } = req.params;
// TODO!
// const secretRotation = (await server.services.secretRotation.triggerSecretRotationRotationSecretsById(
// {
// rotationId,
// type,
// auditLogInfo: req.auditLogInfo
// },
// req.permission
// )) as T;
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// orgId: req.permission.orgId,
// event: {
// type: EventType.DELETE_SECRET_ROTATION,
// metadata: {
// type,
// rotationId,
// removeSecrets
// }
// }
// });
return { secretRotation: null };
}
});
server.route({
method: "POST",
url: "/:rotationId/rotate",
config: {
rateLimit: writeLimit
},
schema: {
description: `Rotate the credentials for the specified ${rotationType} Rotation.`,
params: z.object({
rotationId: z.string().uuid().describe(SecretRotations.ROTATE(type).rotationId)
}),
response: {
200: z.object({ secretRotation: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { rotationId } = req.params;
// TODO!
// const secretRotation = (await server.services.secretRotation.triggerSecretRotationRotationSecretsById(
// {
// rotationId,
// type,
// auditLogInfo: req.auditLogInfo
// },
// req.permission
// )) as T;
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// orgId: req.permission.orgId,
// event: {
// type: EventType.DELETE_SECRET_ROTATION,
// metadata: {
// type,
// rotationId,
// removeSecrets
// }
// }
// });
return { secretRotation: null };
}
});
};

View File

@ -0,0 +1,81 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
PostgresCredentialsRotationListItemSchema,
PostgresCredentialsRotationSchema
} from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { SecretRotations } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const SecretRotationV2Schema = z.discriminatedUnion("type", [PostgresCredentialsRotationSchema]);
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [PostgresCredentialsRotationListItemSchema]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/options",
config: {
rateLimit: readLimit
},
schema: {
description: "List the available Secret Rotation Options.",
response: {
200: z.object({
secretRotationOptions: SecretRotationV2OptionsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: () => {
const secretRotationOptions = server.services.secretRotationV2.listSecretRotationOptions();
return { secretRotationOptions };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
description: "List all the Secret Rotations for the specified project.",
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.LIST().projectId)
}),
response: {
200: z.object({ secretRotations: SecretRotationV2Schema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId },
permission
} = req;
const secretRotations = await server.services.secretRotationV2.listSecretRotationsByProjectId(
{ projectId },
permission
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_SECRET_ROTATIONS,
metadata: {
rotationIds: secretRotations.map((sync) => sync.id),
count: secretRotations.length
}
}
});
return { secretRotations };
}
});
};

View File

@ -2,6 +2,13 @@ import {
TCreateProjectTemplateDTO,
TUpdateProjectTemplateDTO
} from "@app/ee/services/project-template/project-template-types";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
TCreateSecretRotationV2DTO,
TDeleteSecretRotationV2DTO,
TSecretRotationV2,
TUpdateSecretRotationV2DTO
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
@ -283,7 +290,15 @@ export enum EventType {
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
KMIP_OPERATION_REGISTER = "kmip-operation-register"
KMIP_OPERATION_REGISTER = "kmip-operation-register",
GET_SECRET_ROTATIONS = "get-secret-rotations",
GET_SECRET_ROTATION = "get-secret-rotation",
GET_SECRET_ROTATION_CREDENTIALS = "get-secret-rotation-credentials",
CREATE_SECRET_ROTATION = "create-secret-rotation",
UPDATE_SECRET_ROTATION = "update-secret-rotation",
DELETE_SECRET_ROTATION = "delete-secret-rotation",
ROTATE_SECRET_ROTATION = "rotate-secret-rotation"
}
interface UserActorMetadata {
@ -2285,6 +2300,56 @@ interface RegisterKmipServerEvent {
};
}
interface GetSecretRotationsEvent {
type: EventType.GET_SECRET_ROTATIONS;
metadata: {
type?: SecretRotation;
count: number;
rotationIds: string[];
};
}
interface GetSecretRotationEvent {
type: EventType.GET_SECRET_ROTATION;
metadata: {
type: SecretRotation;
rotationId: string;
};
}
interface GetSecretRotationCredentialsEvent {
type: EventType.GET_SECRET_ROTATION_CREDENTIALS;
metadata: {
type: SecretRotation;
rotationId: string;
};
}
interface CreateSecretRotationEvent {
type: EventType.CREATE_SECRET_ROTATION;
metadata: Omit<TCreateSecretRotationV2DTO, "projectId"> & { rotationId: string };
}
interface UpdateSecretRotationEvent {
type: EventType.UPDATE_SECRET_ROTATION;
metadata: TUpdateSecretRotationV2DTO;
}
interface DeleteSecretRotationEvent {
type: EventType.DELETE_SECRET_ROTATION;
metadata: TDeleteSecretRotationV2DTO;
}
interface RotateSecretRotationEvent {
type: EventType.ROTATE_SECRET_ROTATION;
metadata: Pick<TSecretRotationV2, "parameters" | "type" | "rotationStatus" | "connectionId" | "folderId"> & {
rotationId: string;
rotationMessage: string | null;
jobId?: string;
rotatedAt: Date;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -2495,4 +2560,11 @@ export type Event =
| KmipOperationLocateEvent
| KmipOperationRegisterEvent
| CreateSecretRequestEvent
| SecretApprovalRequestReview;
| SecretApprovalRequestReview
| GetSecretRotationsEvent
| GetSecretRotationEvent
| GetSecretRotationCredentialsEvent
| CreateSecretRotationEvent
| UpdateSecretRotationEvent
| DeleteSecretRotationEvent
| RotateSecretRotationEvent;

View File

@ -53,6 +53,15 @@ export enum ProjectPermissionSecretSyncActions {
RemoveSecrets = "remove-secrets"
}
export enum ProjectPermissionSecretRotationActions {
Read = "read",
ReadCredentials = "read-credentials",
Create = "create",
Edit = "edit",
Delete = "delete",
Rotate = "rotate"
}
export enum ProjectPermissionKmipActions {
CreateClients = "create-clients",
UpdateClients = "update-clients",
@ -160,7 +169,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionSecretRotationActions, ProjectPermissionSub.SecretRotation]
| [
ProjectPermissionActions,
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
@ -278,7 +287,7 @@ const GeneralPermissionSchema = [
}),
z.object({
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretRotationActions).describe(
"Describe what action an entity can take."
)
}),
@ -530,7 +539,6 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval,
ProjectPermissionSub.SecretRotation,
ProjectPermissionSub.Member,
ProjectPermissionSub.Groups,
ProjectPermissionSub.Role,
@ -624,6 +632,18 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.Kmip
);
can(
[
ProjectPermissionSecretRotationActions.Create,
ProjectPermissionSecretRotationActions.Edit,
ProjectPermissionSecretRotationActions.Delete,
ProjectPermissionSecretRotationActions.Read,
ProjectPermissionSecretRotationActions.ReadCredentials,
ProjectPermissionSecretRotationActions.Rotate
],
ProjectPermissionSub.SecretRotation
);
return rules;
};
@ -673,7 +693,7 @@ const buildMemberPermissionRules = () => {
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
@ -819,7 +839,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);

View File

@ -0,0 +1,4 @@
export * from "./postgres-credentials-rotation-constants";
export * from "./postgres-credentials-rotation-fns";
export * from "./postgres-credentials-rotation-schemas";
export * from "./postgres-credentials-rotation-types";

View File

@ -0,0 +1,9 @@
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 POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
name: "PostgreSQL Credentials",
type: SecretRotation.PostgresCredentials,
connection: AppConnection.Postgres
};

View File

@ -0,0 +1,66 @@
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 { AppConnection } from "@app/services/app-connection/app-connection-enums";
const PostgresCredentialsRotationParametersSchema = z.object({
usernameSecretKey: z
.string()
.trim()
.min(1, "Username Secret Key Required")
.describe(SecretRotations.PARAMETERS.POSTGRES_CREDENTIALS.usernameSecretKey),
passwordSecretKey: z
.string()
.trim()
.min(1, "Username Secret Key Required")
.describe(SecretRotations.PARAMETERS.POSTGRES_CREDENTIALS.passwordSecretKey),
issueStatement: z
.string()
.trim()
.min(1, "Issue Credentials SQL Statement Required")
.describe(SecretRotations.PARAMETERS.POSTGRES_CREDENTIALS.issueStatement),
revokeStatement: z
.string()
.trim()
.min(1, "Revoke Credentials SQL Statement Required")
.describe(SecretRotations.PARAMETERS.POSTGRES_CREDENTIALS.revokeStatement)
});
const PostgresCredentialsRotationGeneratedCredentialsSchema = z
.object({
username: z.string(),
password: z.string()
})
.array()
.min(1)
.max(2);
export const PostgresCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.PostgresCredentials).extend({
type: z.literal(SecretRotation.PostgresCredentials),
parameters: PostgresCredentialsRotationParametersSchema
// generatedCredentials: PostgresCredentialsRotationGeneratedCredentialsSchema
});
export const CreatePostgresCredentialsRotationSchema = BaseCreateSecretRotationSchema(
SecretRotation.PostgresCredentials
).extend({
parameters: PostgresCredentialsRotationParametersSchema
});
export const UpdatePostgresCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
SecretRotation.PostgresCredentials
).extend({
parameters: PostgresCredentialsRotationParametersSchema.optional()
});
export const PostgresCredentialsRotationListItemSchema = z.object({
name: z.literal("PostgreSQL Credentials"),
connection: z.literal(AppConnection.Postgres),
type: z.literal(SecretRotation.PostgresCredentials)
});

View File

@ -0,0 +1,19 @@
import { z } from "zod";
import { TPostgresConnection } from "@app/services/app-connection/postgres";
import {
CreatePostgresCredentialsRotationSchema,
PostgresCredentialsRotationListItemSchema,
PostgresCredentialsRotationSchema
} from "./postgres-credentials-rotation-schemas";
export type TPostgresCredentialsRotation = z.infer<typeof PostgresCredentialsRotationSchema>;
export type TPostgresCredentialsRotationInput = z.infer<typeof CreatePostgresCredentialsRotationSchema>;
export type TPostgresCredentialsRotationListItem = z.infer<typeof PostgresCredentialsRotationListItemSchema>;
export type TPostgresCredentialsRotationWithConnection = TPostgresCredentialsRotation & {
connection: TPostgresConnection;
};

View File

@ -0,0 +1,206 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { TSecretRotationsV2 } from "@app/db/schemas/secret-rotations-v2";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
export type TSecretRotationV2DALFactory = ReturnType<typeof secretRotationV2DALFactory>;
type TSecretRotationFindFilter = Parameters<typeof buildFindFilter<TSecretRotationsV2>>[0];
const baseSecretRotationV2Query = ({
filter,
db,
tx
}: {
db: TDbClient;
filter?: TSecretRotationFindFilter;
tx?: Knex;
}) => {
const query = (tx || db.replicaNode())(TableName.SecretRotationV2)
.leftJoin(TableName.SecretFolder, `${TableName.SecretRotationV2}.folderId`, `${TableName.SecretFolder}.id`)
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.join(TableName.AppConnection, `${TableName.SecretRotationV2}.connectionId`, `${TableName.AppConnection}.id`)
.select(selectAllTableCols(TableName.SecretRotationV2))
.select(
// environment
db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
// entire connection
db.ref("name").withSchema(TableName.AppConnection).as("connectionName"),
db.ref("method").withSchema(TableName.AppConnection).as("connectionMethod"),
db.ref("app").withSchema(TableName.AppConnection).as("connectionApp"),
db.ref("orgId").withSchema(TableName.AppConnection).as("connectionOrgId"),
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
db.ref("isPlatformManaged").withSchema(TableName.AppConnection).as("connectionIsPlatformManaged")
);
if (filter) {
/* eslint-disable @typescript-eslint/no-misused-promises */
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretRotationV2, filter)));
}
return query;
};
const expandSecretRotation = (
secretRotation: Awaited<ReturnType<typeof baseSecretRotationV2Query>>[number],
folder?: Awaited<ReturnType<TSecretFolderDALFactory["findSecretPathByFolderIds"]>>[number]
) => {
const {
envId,
envName,
envSlug,
connectionApp,
connectionName,
connectionId,
connectionOrgId,
connectionEncryptedCredentials,
connectionMethod,
connectionDescription,
connectionCreatedAt,
connectionUpdatedAt,
connectionVersion,
connectionIsPlatformManaged,
...el
} = secretRotation;
return {
...el,
connectionId,
environment: envId ? { id: envId, name: envName, slug: envSlug } : null,
connection: {
app: connectionApp,
id: connectionId,
name: connectionName,
orgId: connectionOrgId,
encryptedCredentials: connectionEncryptedCredentials,
method: connectionMethod,
description: connectionDescription,
createdAt: connectionCreatedAt,
updatedAt: connectionUpdatedAt,
version: connectionVersion,
isPlatformManaged: connectionIsPlatformManaged
},
folder: folder
? {
id: folder.id,
path: folder.path
}
: null
};
};
export const secretRotationV2DALFactory = (
db: TDbClient,
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds">
) => {
const secretRotationV2Orm = ormify(db, TableName.SecretRotationV2);
const find = async (
filter: Parameters<(typeof secretRotationV2Orm)["find"]>[0] & { projectId: string },
tx?: Knex
) => {
try {
const secretRotations = await baseSecretRotationV2Query({ filter, db, tx });
if (!secretRotations.length) return [];
const foldersWithPath = await folderDAL.findSecretPathByFolderIds(
filter.projectId,
secretRotations.filter((rotation) => Boolean(rotation.folderId)).map((rotation) => rotation.folderId!)
);
const folderRecord: Record<string, (typeof foldersWithPath)[number]> = {};
foldersWithPath.forEach((folder) => {
if (folder) folderRecord[folder.id] = folder;
});
return secretRotations.map((rotation) =>
expandSecretRotation(rotation, rotation.folderId ? folderRecord[rotation.folderId] : undefined)
);
} catch (error) {
throw new DatabaseError({ error, name: "Find - Secret Rotation V2" });
}
};
const findById = async (id: string, tx?: Knex) => {
try {
const secretRotation = await baseSecretRotationV2Query({
filter: { id },
db,
tx
}).first();
if (secretRotation) {
const [folderWithPath] = secretRotation.folderId
? await folderDAL.findSecretPathByFolderIds(secretRotation.projectId, [secretRotation.folderId])
: [];
return expandSecretRotation(secretRotation, folderWithPath);
}
} catch (error) {
throw new DatabaseError({ error, name: "Find by ID - Secret Rotation V2" });
}
};
const create = async (data: Parameters<(typeof secretRotationV2Orm)["create"]>[0]) => {
const secretRotation = (await secretRotationV2Orm.transaction(async (tx) => {
const rotation = await secretRotationV2Orm.create(data, tx);
return baseSecretRotationV2Query({
filter: { id: rotation.id },
db,
tx
}).first();
}))!;
const [folderWithPath] = secretRotation.folderId
? await folderDAL.findSecretPathByFolderIds(secretRotation.projectId, [secretRotation.folderId])
: [];
return expandSecretRotation(secretRotation, folderWithPath);
};
const updateById = async (rotationId: string, data: Parameters<(typeof secretRotationV2Orm)["updateById"]>[1]) => {
const secretRotation = (await secretRotationV2Orm.transaction(async (tx) => {
const rotation = await secretRotationV2Orm.updateById(rotationId, data, tx);
return baseSecretRotationV2Query({
filter: { id: rotation.id },
db,
tx
}).first();
}))!;
const [folderWithPath] = secretRotation.folderId
? await folderDAL.findSecretPathByFolderIds(secretRotation.projectId, [secretRotation.folderId])
: [];
return expandSecretRotation(secretRotation, folderWithPath);
};
const findOne = async (filter: Parameters<(typeof secretRotationV2Orm)["findOne"]>[0], tx?: Knex) => {
try {
const secretRotation = await baseSecretRotationV2Query({ filter, db, tx }).first();
if (secretRotation) {
const [folderWithPath] = secretRotation.folderId
? await folderDAL.findSecretPathByFolderIds(secretRotation.projectId, [secretRotation.folderId])
: [];
return expandSecretRotation(secretRotation, folderWithPath);
}
} catch (error) {
throw new DatabaseError({ error, name: "Find One - Secret Rotation V2" });
}
};
return { ...secretRotationV2Orm, find, create, findById, updateById, findOne };
};

View File

@ -0,0 +1,4 @@
export enum SecretRotation {
PostgresCredentials = "postgres-credentials",
MsSqlCredentials = "mssql-login-credentials"
}

View File

@ -0,0 +1,237 @@
// import { AxiosError } from "axios";
//
// import {
// AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
// AwsParameterStoreSyncFns
// } from "@app/services/secret-sync/aws-parameter-store";
// import {
// AWS_SECRETS_MANAGER_SYNC_LIST_OPTION,
// AwsSecretsManagerSyncFns
// } from "@app/services/secret-sync/aws-secrets-manager";
// import { DATABRICKS_SYNC_LIST_OPTION, databricksSyncFactory } from "@app/services/secret-sync/databricks";
// import { GITHUB_SYNC_LIST_OPTION, GithubSyncFns } from "@app/services/secret-sync/github";
// import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
// import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
// import {
// TSecretMap,
// TSecretSyncListItem,
// TSecretSyncWithCredentials
// } from "@app/services/secret-sync/secret-sync-types";
//
// import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
// import { TKmsServiceFactory } from "../kms/kms-service";
// import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration";
// import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
// import { GCP_SYNC_LIST_OPTION } from "./gcp";
// import { GcpSyncFns } from "./gcp/gcp-sync-fns";
// import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
// import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
//
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
import { SecretRotation } from "./secret-rotation-v2-enums";
import { TSecretRotationV2ListItem } from "./secret-rotation-v2-types";
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.MsSqlCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION // TODO: replace
};
export const listSecretRotationOptions = () => {
return Object.values(SECRET_ROTATION_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name));
};
//
// type TSyncSecretDeps = {
// appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
// kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
// };
//
// // const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
// // let secretMap = { ...unprocessedSecretMap };
// //
// // const { appendSuffix, prependPrefix } = secretSync.syncOptions;
// //
// // if (appendSuffix || prependPrefix) {
// // secretMap = {};
// // Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
// // secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value;
// // });
// // }
// //
// // return secretMap;
// // };
// //
// // const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
// // let secretMap = { ...unprocessedSecretMap };
// //
// // const { appendSuffix, prependPrefix } = secretSync.syncOptions;
// //
// // if (appendSuffix || prependPrefix) {
// // secretMap = {};
// // Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
// // let processedKey = key;
// //
// // if (prependPrefix && processedKey.startsWith(prependPrefix)) {
// // processedKey = processedKey.slice(prependPrefix.length);
// // }
// //
// // if (appendSuffix && processedKey.endsWith(appendSuffix)) {
// // processedKey = processedKey.slice(0, -appendSuffix.length);
// // }
// //
// // secretMap[processedKey] = value;
// // });
// // }
// //
// // return secretMap;
// // };
//
// export const SecretRotationV2Fns = {
// syncSecrets: (
// secretSync: TSecretSyncWithCredentials,
// secretMap: TSecretMap,
// { kmsService, appConnectionDAL }: TSyncSecretDeps
// ): Promise<void> => {
// // const affixedSecretMap = addAffixes(secretSync, secretMap);
//
// switch (secretSync.destination) {
// case SecretSync.AWSParameterStore:
// return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
// case SecretSync.AWSSecretsManager:
// return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap);
// case SecretSync.GitHub:
// return GithubSyncFns.syncSecrets(secretSync, secretMap);
// case SecretSync.GCPSecretManager:
// return GcpSyncFns.syncSecrets(secretSync, secretMap);
// case SecretSync.AzureKeyVault:
// return azureKeyVaultSyncFactory({
// appConnectionDAL,
// kmsService
// }).syncSecrets(secretSync, secretMap);
// case SecretSync.AzureAppConfiguration:
// return azureAppConfigurationSyncFactory({
// appConnectionDAL,
// kmsService
// }).syncSecrets(secretSync, secretMap);
// case SecretSync.Databricks:
// return databricksSyncFactory({
// appConnectionDAL,
// kmsService
// }).syncSecrets(secretSync, secretMap);
// case SecretSync.Humanitec:
// return HumanitecSyncFns.syncSecrets(secretSync, secretMap);
// default:
// throw new Error(
// `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
// );
// }
// },
// getSecrets: async (
// secretSync: TSecretSyncWithCredentials,
// { kmsService, appConnectionDAL }: TSyncSecretDeps
// ): Promise<TSecretMap> => {
// let secretMap: TSecretMap;
// switch (secretSync.destination) {
// case SecretSync.AWSParameterStore:
// secretMap = await AwsParameterStoreSyncFns.getSecrets(secretSync);
// break;
// case SecretSync.AWSSecretsManager:
// secretMap = await AwsSecretsManagerSyncFns.getSecrets(secretSync);
// break;
// case SecretSync.GitHub:
// secretMap = await GithubSyncFns.getSecrets(secretSync);
// break;
// case SecretSync.GCPSecretManager:
// secretMap = await GcpSyncFns.getSecrets(secretSync);
// break;
// case SecretSync.AzureKeyVault:
// secretMap = await azureKeyVaultSyncFactory({
// appConnectionDAL,
// kmsService
// }).getSecrets(secretSync);
// break;
// case SecretSync.AzureAppConfiguration:
// secretMap = await azureAppConfigurationSyncFactory({
// appConnectionDAL,
// kmsService
// }).getSecrets(secretSync);
// break;
// case SecretSync.Databricks:
// return databricksSyncFactory({
// appConnectionDAL,
// kmsService
// }).getSecrets(secretSync);
// case SecretSync.Humanitec:
// secretMap = await HumanitecSyncFns.getSecrets(secretSync);
// break;
// default:
// throw new Error(
// `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
// );
// }
//
// return secretMap;
// // return stripAffixes(secretSync, secretMap);
// },
// removeSecrets: (
// secretSync: TSecretSyncWithCredentials,
// secretMap: TSecretMap,
// { kmsService, appConnectionDAL }: TSyncSecretDeps
// ): Promise<void> => {
// // const affixedSecretMap = addAffixes(secretSync, secretMap);
//
// switch (secretSync.destination) {
// case SecretSync.AWSParameterStore:
// return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
// case SecretSync.AWSSecretsManager:
// return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap);
// case SecretSync.GitHub:
// return GithubSyncFns.removeSecrets(secretSync, secretMap);
// case SecretSync.GCPSecretManager:
// return GcpSyncFns.removeSecrets(secretSync, secretMap);
// case SecretSync.AzureKeyVault:
// return azureKeyVaultSyncFactory({
// appConnectionDAL,
// kmsService
// }).removeSecrets(secretSync, secretMap);
// case SecretSync.AzureAppConfiguration:
// return azureAppConfigurationSyncFactory({
// appConnectionDAL,
// kmsService
// }).removeSecrets(secretSync, secretMap);
// case SecretSync.Databricks:
// return databricksSyncFactory({
// appConnectionDAL,
// kmsService
// }).removeSecrets(secretSync, secretMap);
// case SecretSync.Humanitec:
// return HumanitecSyncFns.removeSecrets(secretSync, secretMap);
// default:
// throw new Error(
// `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
// );
// }
// }
// };
//
// const MAX_MESSAGE_LENGTH = 1024;
//
// export const parseSyncErrorMessage = (err: unknown): string => {
// let errorMessage: string;
//
// if (err instanceof SecretSyncError) {
// errorMessage = JSON.stringify({
// secretKey: err.secretKey,
// error: err.message || parseSyncErrorMessage(err.error)
// });
// } else if (err instanceof AxiosError) {
// errorMessage = err?.response?.data
// ? JSON.stringify(err?.response?.data)
// : err?.message ?? "An unknown error occurred.";
// } else {
// errorMessage = (err as Error)?.message || "An unknown error occurred.";
// }
//
// return errorMessage.length <= MAX_MESSAGE_LENGTH
// ? errorMessage
// : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
// };

View File

@ -0,0 +1,12 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials"
};
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql
};

View File

@ -0,0 +1,972 @@
// import opentelemetry from "@opentelemetry/api";
// import { AxiosError } from "axios";
// import { Job } from "bullmq";
//
// import { ProjectMembershipRole, SecretType } from "@app/db/schemas";
// import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
// import { EventType } from "@app/ee/services/audit-log/audit-log-types";
// import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
// import { getConfig } from "@app/lib/config/env";
// import { logger } from "@app/lib/logger";
// import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
// import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
// import { ActorType } from "@app/services/auth/auth-type";
// import { TKmsServiceFactory } from "@app/services/kms/kms-service";
// import { KmsDataKey } from "@app/services/kms/kms-types";
// import { TProjectDALFactory } from "@app/services/project/project-dal";
// import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
// import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
// import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
// import { TSecretDALFactory } from "@app/services/secret/secret-dal";
// import { createManySecretsRawFnFactory, updateManySecretsRawFnFactory } from "@app/services/secret/secret-fns";
// import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
// import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
// import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
// import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
// import { TSecretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
// import { fnSecretsV2FromImports } from "@app/services/secret-import/secret-import-fns";
// import { TSecretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal";
// import {
// SecretSync,
// SecretSyncImportBehavior,
// SecretSyncInitialSyncBehavior
// } from "@app/services/secret-sync/secret-sync-enums";
// import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
// import { parseSyncErrorMessage, SecretSyncFns } from "@app/services/secret-sync/secret-sync-fns";
// import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
// import {
// SecretSyncAction,
// SecretSyncStatus,
// TQueueSecretSyncImportSecretsByIdDTO,
// TQueueSecretSyncRemoveSecretsByIdDTO,
// TQueueSecretSyncsByPathDTO,
// TQueueSecretSyncSyncSecretsByIdDTO,
// TQueueSendSecretSyncActionFailedNotificationsDTO,
// TSecretMap,
// TSecretSyncImportSecretsDTO,
// TSecretSyncRaw,
// TSecretSyncRemoveSecretsDTO,
// TSecretSyncSyncSecretsDTO,
// TSecretSyncWithCredentials,
// TSendSecretSyncFailedNotificationsJobDTO
// } from "@app/services/secret-sync/secret-sync-types";
// import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
// import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
// import { expandSecretReferencesFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
// import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
// import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
// import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
//
// import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
//
// export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
//
// type TSecretSyncQueueFactoryDep = {
// queueService: Pick<TQueueServiceFactory, "queue" | "start">;
// kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
// appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
// keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
// folderDAL: TSecretFolderDALFactory;
// secretV2BridgeDAL: Pick<
// TSecretV2BridgeDALFactory,
// | "findByFolderId"
// | "find"
// | "insertMany"
// | "upsertSecretReferences"
// | "findBySecretKeys"
// | "bulkUpdate"
// | "deleteMany"
// >;
// secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
// secretSyncDAL: Pick<TSecretSyncDALFactory, "findById" | "find" | "updateById" | "deleteById">;
// auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
// projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
// projectDAL: TProjectDALFactory;
// smtpService: Pick<TSmtpService, "sendMail">;
// projectBotDAL: TProjectBotDALFactory;
// secretDAL: TSecretDALFactory;
// secretVersionDAL: TSecretVersionDALFactory;
// secretBlindIndexDAL: TSecretBlindIndexDALFactory;
// secretTagDAL: TSecretTagDALFactory;
// secretVersionTagDAL: TSecretVersionTagDALFactory;
// secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
// secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
// resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
// };
//
// type SecretSyncActionJob = Job<
// TQueueSecretSyncSyncSecretsByIdDTO | TQueueSecretSyncImportSecretsByIdDTO | TQueueSecretSyncRemoveSecretsByIdDTO
// >;
//
// const getRequeueDelay = (failureCount?: number) => {
// if (!failureCount) return 0;
//
// const baseDelay = 1000;
// const maxDelay = 30000;
//
// const delay = Math.min(baseDelay * 2 ** failureCount, maxDelay);
//
// const jitter = delay * (0.5 + Math.random() * 0.5);
//
// return jitter;
// };
//
// export const secretSyncQueueFactory = ({
// queueService,
// kmsService,
// appConnectionDAL,
// keyStore,
// folderDAL,
// secretV2BridgeDAL,
// secretImportDAL,
// secretSyncDAL,
// auditLogService,
// projectMembershipDAL,
// projectDAL,
// smtpService,
// projectBotDAL,
// secretDAL,
// secretVersionDAL,
// secretBlindIndexDAL,
// secretTagDAL,
// secretVersionTagDAL,
// secretVersionV2BridgeDAL,
// secretVersionTagV2BridgeDAL,
// resourceMetadataDAL
// }: TSecretSyncQueueFactoryDep) => {
// const appCfg = getConfig();
//
// const integrationMeter = opentelemetry.metrics.getMeter("SecretSyncs");
// const syncSecretsErrorHistogram = integrationMeter.createHistogram("secret_sync_sync_secrets_errors", {
// description: "Secret Sync - sync secrets errors",
// unit: "1"
// });
// const importSecretsErrorHistogram = integrationMeter.createHistogram("secret_sync_import_secrets_errors", {
// description: "Secret Sync - import secrets errors",
// unit: "1"
// });
// const removeSecretsErrorHistogram = integrationMeter.createHistogram("secret_sync_remove_secrets_errors", {
// description: "Secret Sync - remove secrets errors",
// unit: "1"
// });
//
// const $createManySecretsRawFn = createManySecretsRawFnFactory({
// projectDAL,
// projectBotDAL,
// secretDAL,
// secretVersionDAL,
// secretBlindIndexDAL,
// secretTagDAL,
// secretVersionTagDAL,
// folderDAL,
// kmsService,
// secretVersionV2BridgeDAL,
// secretV2BridgeDAL,
// secretVersionTagV2BridgeDAL,
// resourceMetadataDAL
// });
//
// const $updateManySecretsRawFn = updateManySecretsRawFnFactory({
// projectDAL,
// projectBotDAL,
// secretDAL,
// secretVersionDAL,
// secretBlindIndexDAL,
// secretTagDAL,
// secretVersionTagDAL,
// folderDAL,
// kmsService,
// secretVersionV2BridgeDAL,
// secretV2BridgeDAL,
// secretVersionTagV2BridgeDAL,
// resourceMetadataDAL
// });
//
// const $getInfisicalSecrets = async (
// secretSync: TSecretSyncRaw | TSecretSyncWithCredentials,
// includeImports = true
// ) => {
// const { projectId, folderId, environment, folder } = secretSync;
//
// if (!folderId || !environment || !folder)
// throw new SecretSyncError({
// message:
// "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path.",
// shouldRetry: false
// });
//
// const secretMap: TSecretMap = {};
//
// const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
// type: KmsDataKey.SecretManager,
// projectId
// });
//
// const decryptSecretValue = (value?: Buffer | undefined | null) =>
// value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "";
//
// const { expandSecretReferences } = expandSecretReferencesFactory({
// decryptSecretValue,
// secretDAL: secretV2BridgeDAL,
// folderDAL,
// projectId,
// canExpandValue: () => true
// });
//
// const secrets = await secretV2BridgeDAL.findByFolderId(folderId);
//
// await Promise.allSettled(
// secrets.map(async (secret) => {
// const secretKey = secret.key;
// const secretValue = decryptSecretValue(secret.encryptedValue);
// const expandedSecretValue = await expandSecretReferences({
// environment: environment.slug,
// secretPath: folder.path,
// skipMultilineEncoding: secret.skipMultilineEncoding,
// value: secretValue
// });
// secretMap[secretKey] = { value: expandedSecretValue || "" };
//
// if (secret.encryptedComment) {
// const commentValue = decryptSecretValue(secret.encryptedComment);
// secretMap[secretKey].comment = commentValue;
// }
//
// secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
// secretMap[secretKey].secretMetadata = secret.secretMetadata;
// })
// );
//
// if (!includeImports) return secretMap;
//
// const secretImports = await secretImportDAL.find({ folderId, isReplication: false });
//
// if (secretImports.length) {
// const importedSecrets = await fnSecretsV2FromImports({
// decryptor: decryptSecretValue,
// folderDAL,
// secretDAL: secretV2BridgeDAL,
// expandSecretReferences,
// secretImportDAL,
// secretImports,
// hasSecretAccess: () => true,
// viewSecretValue: true
// });
//
// for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
// for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
// const importedSecret = importedSecrets[i].secrets[j];
// if (!secretMap[importedSecret.key]) {
// secretMap[importedSecret.key] = {
// skipMultilineEncoding: importedSecret.skipMultilineEncoding,
// comment: importedSecret.secretComment,
// value: importedSecret.secretValue || "",
// secretMetadata: importedSecret.secretMetadata
// };
// }
// }
// }
// }
//
// return secretMap;
// };
//
// const queueSecretSyncSyncSecretsById = async (payload: TQueueSecretSyncSyncSecretsByIdDTO) =>
// queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncSyncSecrets, payload, {
// delay: getRequeueDelay(payload.failedToAcquireLockCount), // this is for delaying re-queued jobs if sync is locked
// attempts: 5,
// backoff: {
// type: "exponential",
// delay: 3000
// },
// removeOnComplete: true,
// removeOnFail: true
// });
//
// const queueSecretSyncImportSecretsById = async (payload: TQueueSecretSyncImportSecretsByIdDTO) =>
// queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncImportSecrets, payload, {
// attempts: 1,
// removeOnComplete: true,
// removeOnFail: true
// });
//
// const queueSecretSyncRemoveSecretsById = async (payload: TQueueSecretSyncRemoveSecretsByIdDTO) =>
// queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncRemoveSecrets, payload, {
// attempts: 1,
// removeOnComplete: true,
// removeOnFail: true
// });
//
// const $queueSendSecretSyncFailedNotifications = async (payload: TQueueSendSecretSyncActionFailedNotificationsDTO) => {
// if (!appCfg.isSmtpConfigured) return;
//
// await queueService.queue(
// QueueName.AppConnectionSecretSync,
// QueueJobs.SecretSyncSendActionFailedNotifications,
// payload,
// {
// jobId: `secret-sync-${payload.secretSync.id}-failed-notifications`,
// attempts: 5,
// delay: 1000 * 60,
// backoff: {
// type: "exponential",
// delay: 3000
// },
// removeOnFail: true,
// removeOnComplete: true
// }
// );
// };
//
// const $importSecrets = async (
// secretSync: TSecretSyncWithCredentials,
// importBehavior: SecretSyncImportBehavior
// ): Promise<TSecretMap> => {
// const { projectId, environment, folder } = secretSync;
//
// if (!environment || !folder)
// throw new Error(
// "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path."
// );
//
// const importedSecrets = await SecretSyncFns.getSecrets(secretSync, {
// appConnectionDAL,
// kmsService
// });
//
// if (!Object.keys(importedSecrets).length) return {};
//
// const importedSecretMap: TSecretMap = {};
//
// const secretMap = await $getInfisicalSecrets(secretSync, false);
//
// const secretsToCreate: Parameters<typeof $createManySecretsRawFn>[0]["secrets"] = [];
// const secretsToUpdate: Parameters<typeof $updateManySecretsRawFn>[0]["secrets"] = [];
//
// Object.entries(importedSecrets).forEach(([key, secretData]) => {
// const { value, comment = "", skipMultilineEncoding } = secretData;
//
// const secret = {
// secretName: key,
// secretValue: value,
// type: SecretType.Shared,
// secretComment: comment,
// skipMultilineEncoding: skipMultilineEncoding ?? undefined
// };
//
// if (Object.hasOwn(secretMap, key)) {
// secretsToUpdate.push(secret);
// if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination) importedSecretMap[key] = secretData;
// } else {
// secretsToCreate.push(secret);
// importedSecretMap[key] = secretData;
// }
// });
//
// if (secretsToCreate.length) {
// await $createManySecretsRawFn({
// projectId,
// path: folder.path,
// environment: environment.slug,
// secrets: secretsToCreate
// });
// }
//
// if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination && secretsToUpdate.length) {
// await $updateManySecretsRawFn({
// projectId,
// path: folder.path,
// environment: environment.slug,
// secrets: secretsToUpdate
// });
// }
//
// return importedSecretMap;
// };
//
// const $handleSyncSecretsJob = async (job: TSecretSyncSyncSecretsDTO) => {
// const {
// data: { syncId, auditLogInfo }
// } = job;
//
// const secretSync = await secretSyncDAL.findById(syncId);
//
// if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
//
// await secretSyncDAL.updateById(syncId, {
// syncStatus: SecretSyncStatus.Running
// });
//
// logger.info(
// `SecretSync Sync [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
// );
//
// let isSynced = false;
// let syncMessage: string | null = null;
// let isFinalAttempt = job.attemptsStarted === job.opts.attempts;
//
// try {
// const {
// connection: { orgId, encryptedCredentials }
// } = secretSync;
//
// const credentials = await decryptAppConnectionCredentials({
// orgId,
// encryptedCredentials,
// kmsService
// });
//
// const secretSyncWithCredentials = {
// ...secretSync,
// connection: {
// ...secretSync.connection,
// credentials
// }
// } as TSecretSyncWithCredentials;
//
// const {
// lastSyncedAt,
// syncOptions: { initialSyncBehavior }
// } = secretSyncWithCredentials;
//
// const secretMap = await $getInfisicalSecrets(secretSync);
//
// if (!lastSyncedAt && initialSyncBehavior !== SecretSyncInitialSyncBehavior.OverwriteDestination) {
// const importedSecretMap = await $importSecrets(
// secretSyncWithCredentials,
// initialSyncBehavior === SecretSyncInitialSyncBehavior.ImportPrioritizeSource
// ? SecretSyncImportBehavior.PrioritizeSource
// : SecretSyncImportBehavior.PrioritizeDestination
// );
//
// Object.entries(importedSecretMap).forEach(([key, secretData]) => {
// secretMap[key] = secretData;
// });
// }
//
// await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap, {
// appConnectionDAL,
// kmsService
// });
//
// isSynced = true;
// } catch (err) {
// logger.error(
// err,
// `SecretSync Sync Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
// );
//
// if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
// syncSecretsErrorHistogram.record(1, {
// version: 1,
// destination: secretSync.destination,
// syncId: secretSync.id,
// projectId: secretSync.projectId,
// type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
// status: err instanceof AxiosError ? err.response?.status : undefined,
// name: err instanceof Error ? err.name : undefined
// });
// }
//
// syncMessage = parseSyncErrorMessage(err);
//
// if (err instanceof SecretSyncError && !err.shouldRetry) {
// isFinalAttempt = true;
// } else {
// // re-throw so job fails
// throw err;
// }
// } finally {
// const ranAt = new Date();
// const syncStatus = isSynced ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed;
//
// await auditLogService.createAuditLog({
// projectId: secretSync.projectId,
// ...(auditLogInfo ?? {
// actor: {
// type: ActorType.PLATFORM,
// metadata: {}
// }
// }),
// event: {
// type: EventType.SECRET_SYNC_SYNC_SECRETS,
// metadata: {
// syncId: secretSync.id,
// syncOptions: secretSync.syncOptions,
// destination: secretSync.destination,
// destinationConfig: secretSync.destinationConfig,
// folderId: secretSync.folderId,
// connectionId: secretSync.connectionId,
// jobRanAt: ranAt,
// jobId: job.id!,
// syncStatus,
// syncMessage
// }
// }
// });
//
// if (isSynced || isFinalAttempt) {
// const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
// syncStatus,
// lastSyncJobId: job.id,
// lastSyncMessage: syncMessage,
// lastSyncedAt: isSynced ? ranAt : undefined
// });
//
// if (!isSynced) {
// await $queueSendSecretSyncFailedNotifications({
// secretSync: updatedSecretSync,
// action: SecretSyncAction.SyncSecrets,
// auditLogInfo
// });
// }
// }
// }
//
// logger.info("SecretSync Sync Job with ID %s Completed", job.id);
// };
//
// const $handleImportSecretsJob = async (job: TSecretSyncImportSecretsDTO) => {
// const {
// data: { syncId, auditLogInfo, importBehavior }
// } = job;
//
// const secretSync = await secretSyncDAL.findById(syncId);
//
// if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
//
// await secretSyncDAL.updateById(syncId, {
// importStatus: SecretSyncStatus.Running
// });
//
// logger.info(
// `SecretSync Import [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
// );
//
// let isSuccess = false;
// let importMessage: string | null = null;
// const isFinalAttempt = job.attemptsStarted === job.opts.attempts;
//
// try {
// const {
// connection: { orgId, encryptedCredentials }
// } = secretSync;
//
// const credentials = await decryptAppConnectionCredentials({
// orgId,
// encryptedCredentials,
// kmsService
// });
//
// await $importSecrets(
// {
// ...secretSync,
// connection: {
// ...secretSync.connection,
// credentials
// }
// } as TSecretSyncWithCredentials,
// importBehavior
// );
//
// isSuccess = true;
// } catch (err) {
// logger.error(
// err,
// `SecretSync Import Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
// );
//
// if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
// importSecretsErrorHistogram.record(1, {
// version: 1,
// destination: secretSync.destination,
// syncId: secretSync.id,
// projectId: secretSync.projectId,
// type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
// status: err instanceof AxiosError ? err.response?.status : undefined,
// name: err instanceof Error ? err.name : undefined
// });
// }
//
// importMessage = parseSyncErrorMessage(err);
//
// // re-throw so job fails
// throw err;
// } finally {
// const ranAt = new Date();
// const importStatus = isSuccess ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed;
//
// await auditLogService.createAuditLog({
// projectId: secretSync.projectId,
// ...(auditLogInfo ?? {
// actor: {
// type: ActorType.PLATFORM,
// metadata: {}
// }
// }),
// event: {
// type: EventType.SECRET_SYNC_IMPORT_SECRETS,
// metadata: {
// syncId: secretSync.id,
// syncOptions: secretSync.syncOptions,
// destination: secretSync.destination,
// destinationConfig: secretSync.destinationConfig,
// folderId: secretSync.folderId,
// connectionId: secretSync.connectionId,
// jobRanAt: ranAt,
// jobId: job.id!,
// importStatus,
// importMessage,
// importBehavior
// }
// }
// });
//
// if (isSuccess || isFinalAttempt) {
// const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
// importStatus,
// lastImportJobId: job.id,
// lastImportMessage: importMessage,
// lastImportedAt: isSuccess ? ranAt : undefined
// });
//
// if (!isSuccess) {
// await $queueSendSecretSyncFailedNotifications({
// secretSync: updatedSecretSync,
// action: SecretSyncAction.ImportSecrets,
// auditLogInfo
// });
// }
// }
// }
//
// logger.info("SecretSync Import Job with ID %s Completed", job.id);
// };
//
// const $handleRemoveSecretsJob = async (job: TSecretSyncRemoveSecretsDTO) => {
// const {
// data: { syncId, auditLogInfo, deleteSyncOnComplete }
// } = job;
//
// const secretSync = await secretSyncDAL.findById(syncId);
//
// if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
//
// await secretSyncDAL.updateById(syncId, {
// removeStatus: SecretSyncStatus.Running
// });
//
// logger.info(
// `SecretSync Remove [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
// );
//
// let isSuccess = false;
// let removeMessage: string | null = null;
// const isFinalAttempt = job.attemptsStarted === job.opts.attempts;
//
// try {
// const {
// connection: { orgId, encryptedCredentials }
// } = secretSync;
//
// const credentials = await decryptAppConnectionCredentials({
// orgId,
// encryptedCredentials,
// kmsService
// });
//
// const secretMap = await $getInfisicalSecrets(secretSync);
//
// await SecretSyncFns.removeSecrets(
// {
// ...secretSync,
// connection: {
// ...secretSync.connection,
// credentials
// }
// } as TSecretSyncWithCredentials,
// secretMap,
// {
// appConnectionDAL,
// kmsService
// }
// );
//
// isSuccess = true;
// } catch (err) {
// logger.error(
// err,
// `SecretSync Remove Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
// );
//
// if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
// removeSecretsErrorHistogram.record(1, {
// version: 1,
// destination: secretSync.destination,
// syncId: secretSync.id,
// projectId: secretSync.projectId,
// type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
// status: err instanceof AxiosError ? err.response?.status : undefined,
// name: err instanceof Error ? err.name : undefined
// });
// }
//
// removeMessage = parseSyncErrorMessage(err);
//
// // re-throw so job fails
// throw err;
// } finally {
// const ranAt = new Date();
// const removeStatus = isSuccess ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed;
//
// await auditLogService.createAuditLog({
// projectId: secretSync.projectId,
// ...(auditLogInfo ?? {
// actor: {
// type: ActorType.PLATFORM,
// metadata: {}
// }
// }),
// event: {
// type: EventType.SECRET_SYNC_REMOVE_SECRETS,
// metadata: {
// syncId: secretSync.id,
// syncOptions: secretSync.syncOptions,
// destination: secretSync.destination,
// destinationConfig: secretSync.destinationConfig,
// folderId: secretSync.folderId,
// connectionId: secretSync.connectionId,
// jobRanAt: ranAt,
// jobId: job.id!,
// removeStatus,
// removeMessage
// }
// }
// });
//
// if (isSuccess || isFinalAttempt) {
// if (isSuccess && deleteSyncOnComplete) {
// await secretSyncDAL.deleteById(secretSync.id);
// } else {
// const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
// removeStatus,
// lastRemoveJobId: job.id,
// lastRemoveMessage: removeMessage,
// lastRemovedAt: isSuccess ? ranAt : undefined
// });
//
// if (!isSuccess) {
// await $queueSendSecretSyncFailedNotifications({
// secretSync: updatedSecretSync,
// action: SecretSyncAction.RemoveSecrets,
// auditLogInfo
// });
// }
// }
// }
// }
//
// logger.info("SecretSync Remove Job with ID %s Completed", job.id);
// };
//
// const $sendSecretSyncFailedNotifications = async (job: TSendSecretSyncFailedNotificationsJobDTO) => {
// const {
// data: { secretSync, auditLogInfo, action }
// } = job;
//
// const { projectId, destination, name, folder, lastSyncMessage, lastRemoveMessage, lastImportMessage, environment } =
// secretSync;
//
// const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
// const project = await projectDAL.findById(projectId);
//
// let projectAdmins = projectMembers.filter((member) =>
// member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
// );
//
// const triggeredByUserId =
// auditLogInfo && auditLogInfo.actor.type === ActorType.USER && auditLogInfo.actor.metadata.userId;
//
// // only notify triggering user if triggered by admin
// if (triggeredByUserId && projectAdmins.map((admin) => admin.userId).includes(triggeredByUserId)) {
// projectAdmins = projectAdmins.filter((admin) => admin.userId === triggeredByUserId);
// }
//
// const syncDestination = SECRET_SYNC_NAME_MAP[destination as SecretSync];
//
// let actionLabel: string;
// let failureMessage: string | null | undefined;
//
// switch (action) {
// case SecretSyncAction.ImportSecrets:
// actionLabel = "Import";
// failureMessage = lastImportMessage;
//
// break;
// case SecretSyncAction.RemoveSecrets:
// actionLabel = "Remove";
// failureMessage = lastRemoveMessage;
//
// break;
// case SecretSyncAction.SyncSecrets:
// default:
// actionLabel = `Sync`;
// failureMessage = lastSyncMessage;
// break;
// }
//
// await smtpService.sendMail({
// recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
// template: SmtpTemplates.SecretSyncFailed,
// subjectLine: `Secret Sync Failed to ${actionLabel} Secrets`,
// substitutions: {
// syncName: name,
// syncDestination,
// content: `Your ${syncDestination} Sync named "${name}" failed while attempting to ${action.toLowerCase()} secrets.`,
// failureMessage,
// secretPath: folder?.path,
// environment: environment?.name,
// projectName: project.name,
// syncUrl: `${appCfg.SITE_URL}/integrations/secret-syncs/${destination}/${secretSync.id}`
// }
// });
// };
//
// const queueSecretSyncsSyncSecretsByPath = async ({
// secretPath,
// projectId,
// environmentSlug
// }: TQueueSecretSyncsByPathDTO) => {
// const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, secretPath);
//
// if (!folder)
// throw new Error(
// `Could not find folder at path "${secretPath}" for environment with slug "${environmentSlug}" in project with ID "${projectId}"`
// );
//
// const secretSyncs = await secretSyncDAL.find({ folderId: folder.id, isAutoSyncEnabled: true });
//
// await Promise.all(secretSyncs.map((secretSync) => queueSecretSyncSyncSecretsById({ syncId: secretSync.id })));
// };
//
// const $handleAcquireLockFailure = async (job: SecretSyncActionJob) => {
// const { syncId, auditLogInfo } = job.data;
//
// switch (job.name) {
// case QueueJobs.SecretSyncSyncSecrets: {
// const { failedToAcquireLockCount = 0, ...rest } = job.data as TQueueSecretSyncSyncSecretsByIdDTO;
//
// if (failedToAcquireLockCount < 10) {
// await queueSecretSyncSyncSecretsById({ ...rest, failedToAcquireLockCount: failedToAcquireLockCount + 1 });
// return;
// }
//
// const secretSync = await secretSyncDAL.updateById(syncId, {
// syncStatus: SecretSyncStatus.Failed,
// lastSyncMessage:
// "Failed to run job. This typically happens when a sync is already in progress. Please try again.",
// lastSyncJobId: job.id
// });
//
// await $queueSendSecretSyncFailedNotifications({
// secretSync,
// action: SecretSyncAction.SyncSecrets,
// auditLogInfo
// });
//
// break;
// }
// // Scott: the two cases below are unlikely to happen as we check the lock at the API level but including this as a fallback
// case QueueJobs.SecretSyncImportSecrets: {
// const secretSync = await secretSyncDAL.updateById(syncId, {
// importStatus: SecretSyncStatus.Failed,
// lastImportMessage:
// "Failed to run job. This typically happens when a sync is already in progress. Please try again.",
// lastImportJobId: job.id
// });
//
// await $queueSendSecretSyncFailedNotifications({
// secretSync,
// action: SecretSyncAction.ImportSecrets,
// auditLogInfo
// });
//
// break;
// }
// case QueueJobs.SecretSyncRemoveSecrets: {
// const secretSync = await secretSyncDAL.updateById(syncId, {
// removeStatus: SecretSyncStatus.Failed,
// lastRemoveMessage:
// "Failed to run job. This typically happens when a sync is already in progress. Please try again.",
// lastRemoveJobId: job.id
// });
//
// await $queueSendSecretSyncFailedNotifications({
// secretSync,
// action: SecretSyncAction.RemoveSecrets,
// auditLogInfo
// });
//
// break;
// }
// default:
// // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
// throw new Error(`Unhandled Secret Sync Job ${job.name}`);
// }
// };
//
// queueService.start(QueueName.AppConnectionSecretSync, async (job) => {
// if (job.name === QueueJobs.SecretSyncSendActionFailedNotifications) {
// await $sendSecretSyncFailedNotifications(job as TSendSecretSyncFailedNotificationsJobDTO);
// return;
// }
//
// const { syncId } = job.data as
// | TQueueSecretSyncSyncSecretsByIdDTO
// | TQueueSecretSyncImportSecretsByIdDTO
// | TQueueSecretSyncRemoveSecretsByIdDTO;
//
// let lock: Awaited<ReturnType<typeof keyStore.acquireLock>>;
//
// try {
// lock = await keyStore.acquireLock(
// [KeyStorePrefixes.SecretSyncLock(syncId)],
// // scott: not sure on this duration; syncs can take excessive amounts of time so we need to keep it locked,
// // but should always release below...
// 5 * 60 * 1000
// );
// } catch (e) {
// logger.info(`SecretSync Failed to acquire lock [syncId=${syncId}] [job=${job.name}]`);
//
// await $handleAcquireLockFailure(job as SecretSyncActionJob);
//
// return;
// }
//
// try {
// switch (job.name) {
// case QueueJobs.SecretSyncSyncSecrets:
// await $handleSyncSecretsJob(job as TSecretSyncSyncSecretsDTO);
// break;
// case QueueJobs.SecretSyncImportSecrets:
// await $handleImportSecretsJob(job as TSecretSyncImportSecretsDTO);
// break;
// case QueueJobs.SecretSyncRemoveSecrets:
// await $handleRemoveSecretsJob(job as TSecretSyncRemoveSecretsDTO);
// break;
// default:
// // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
// throw new Error(`Unhandled Secret Sync Job ${job.name}`);
// }
// } finally {
// await lock.release();
// }
// });
//
// return {
// queueSecretSyncSyncSecretsById,
// queueSecretSyncImportSecretsById,
// queueSecretSyncRemoveSecretsById,
// queueSecretSyncsSyncSecretsByPath
// };
// };

View File

@ -0,0 +1,69 @@
import { z } from "zod";
import { SecretRotationsV2Schema } from "@app/db/schemas/secret-rotations-v2";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { SECRET_ROTATION_CONNECTION_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
import { SecretRotations } from "@app/lib/api-docs";
import { removeTrailingSlash } from "@app/lib/fn";
import { slugSchema } from "@app/server/lib/schemas";
export const BaseSecretRotationSchema = (type: SecretRotation) =>
SecretRotationsV2Schema.omit({
type: true,
parameters: true,
encryptedGeneratedCredentials: true
}).extend({
connection: z.object({
app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]),
name: z.string(),
id: z.string().uuid()
}),
environment: z.object({ slug: z.string(), name: z.string(), id: z.string().uuid() }).nullable(),
folder: z.object({ id: z.string(), path: z.string() }).nullable()
});
export const BaseCreateSecretRotationSchema = (type: SecretRotation) =>
z.object({
name: slugSchema({ field: "name" }).describe(SecretRotations.CREATE(type).name),
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.CREATE(type).projectId),
description: z
.string()
.trim()
.max(256, "Description cannot exceed 256 characters")
.nullish()
.describe(SecretRotations.CREATE(type).description),
connectionId: z.string().uuid().describe(SecretRotations.CREATE(type).connectionId),
environment: slugSchema({ field: "environment", max: 64 }).describe(SecretRotations.CREATE(type).environment),
secretPath: z
.string()
.trim()
.min(1, "Secret path required")
.transform(removeTrailingSlash)
.describe(SecretRotations.CREATE(type).secretPath),
isAutoRotationEnabled: z
.boolean()
.optional()
.default(true)
.describe(SecretRotations.CREATE(type).isAutoRotationEnabled),
interval: z.coerce.number().describe(SecretRotations.CREATE(type).interval)
});
export const BaseUpdateSecretRotationSchema = (type: SecretRotation) =>
z.object({
name: slugSchema({ field: "name" }).describe(SecretRotations.UPDATE(type).name),
description: z
.string()
.trim()
.max(256, "Description cannot exceed 256 characters")
.nullish()
.describe(SecretRotations.UPDATE(type).description),
environment: slugSchema({ field: "environment", max: 64 }).describe(SecretRotations.UPDATE(type).environment),
secretPath: z
.string()
.trim()
.min(1, "Secret path required")
.transform(removeTrailingSlash)
.describe(SecretRotations.UPDATE(type).secretPath),
isAutoRotationEnabled: z.boolean().default(true).describe(SecretRotations.UPDATE(type).isAutoRotationEnabled),
interval: z.coerce.number().describe(SecretRotations.UPDATE(type).interval)
});

View File

@ -0,0 +1,548 @@
import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionSecretActions,
ProjectPermissionSecretRotationActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { listSecretRotationOptions } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-fns";
import {
SECRET_ROTATION_CONNECTION_MAP,
SECRET_ROTATION_NAME_MAP
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
import {
TCreateSecretRotationV2DTO,
TDeleteSecretRotationV2DTO,
TFindSecretRotationV2ByIdDTO,
TFindSecretRotationV2ByNameDTO,
TListSecretRotationsV2ByProjectId,
TSecretRotationV2,
TUpdateSecretRotationV2DTO
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
type TSecretRotationV2ServiceFactoryDep = {
secretRotationV2DAL: TSecretRotationV2DALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">;
// keyStore: Pick<TKeyStoreFactory, "getItem">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
export const secretRotationV2ServiceFactory = ({
secretRotationV2DAL,
folderDAL,
permissionService,
appConnectionService,
projectBotService,
licenseService
}: TSecretRotationV2ServiceFactoryDep) => {
const listSecretRotationsByProjectId = async (
{ projectId, type }: TListSecretRotationsV2ByProjectId,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretRotation)
throw new BadRequestError({
message: "Failed to access secret rotations due to plan restriction. Upgrade plan to access secret rotations."
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretManager,
projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretRotationActions.Read,
ProjectPermissionSub.SecretRotation
);
const secretRotations = await secretRotationV2DAL.find({
...(type && { type }),
projectId
});
return secretRotations as TSecretRotationV2[];
};
const findSecretRotationById = async ({ type, rotationId }: TFindSecretRotationV2ByIdDTO, actor: OrgServiceActor) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretRotation)
throw new BadRequestError({
message: "Failed to access secret rotation due to plan restriction. Upgrade plan to access secret rotations."
});
const secretRotation = await secretRotationV2DAL.findById(rotationId);
if (!secretRotation)
throw new NotFoundError({
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretManager,
projectId: secretRotation.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretRotationActions.Read,
ProjectPermissionSub.SecretRotation
);
if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
throw new BadRequestError({
message: `Secret Rotation with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
});
return secretRotation as TSecretRotationV2;
};
const findSecretRotationByName = async (
{ type, rotationName, projectId }: TFindSecretRotationV2ByNameDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretRotation)
throw new BadRequestError({
message: "Failed to access secret rotation due to plan restriction. Upgrade plan to access secret rotations."
});
// we prevent conflicting names within a project
const secretRotation = await secretRotationV2DAL.findOne({
name: rotationName,
projectId
});
if (!secretRotation)
throw new NotFoundError({
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with name "${rotationName}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretManager,
projectId: secretRotation.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretRotationActions.Read,
ProjectPermissionSub.SecretRotation
);
if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
throw new BadRequestError({
message: `Secret Rotation with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
});
return secretRotation as TSecretRotationV2;
};
const createSecretRotation = async (
{ projectId, secretPath, environment, ...params }: TCreateSecretRotationV2DTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretRotation)
throw new BadRequestError({
message: "Failed to create secret rotation due to plan restriction. Upgrade plan to create secret rotations."
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretManager,
projectId
});
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (!shouldUseSecretV2Bridge)
throw new BadRequestError({ message: "Project version does not support Secret Rotation V2" });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretRotationActions.Create,
ProjectPermissionSub.SecretRotation
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
throw new BadRequestError({
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
});
const typeApp = SECRET_ROTATION_CONNECTION_MAP[params.type];
// validates permission to connect and app is valid for sync type
await appConnectionService.connectAppConnectionById(typeApp, params.connectionId, actor);
// TODO: initialize credentials
try {
const secretRotation = await secretRotationV2DAL.create({
folderId: folder.id,
...params,
encryptedGeneratedCredentials: Buffer.from([]),
projectId
});
return secretRotation as TSecretRotationV2;
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({
message: `A Secret Rotation with the name "${params.name}" already exists for the project with ID "${folder.projectId}"`
});
}
throw err;
}
};
const updateSecretRotation = async (
{ type, rotationId, secretPath, environment, ...params }: TUpdateSecretRotationV2DTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretRotation)
throw new BadRequestError({
message: "Failed to update secret rotation due to plan restriction. Upgrade plan to update secret rotations."
});
const secretRotation = await secretRotationV2DAL.findById(rotationId);
if (!secretRotation)
throw new NotFoundError({
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID ${rotationId}`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretManager,
projectId: secretRotation.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretRotationActions.Edit,
ProjectPermissionSub.SecretRotation
);
if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
throw new BadRequestError({
message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
});
let { folderId } = secretRotation;
if (
(secretPath && secretPath !== secretRotation.folder?.path) ||
(environment && environment !== secretRotation.environment?.slug)
) {
const updatedEnvironment = environment ?? secretRotation.environment?.slug;
const updatedSecretPath = secretPath ?? secretRotation.folder?.path;
if (!updatedEnvironment || !updatedSecretPath)
throw new BadRequestError({ message: "Must specify both source environment and secret path" });
// TODO: get secrets to determine delete permission
// ForbiddenError.from(permission).throwUnlessCan(
// ProjectPermissionSecretActions.Create,
// subject(ProjectPermissionSub.Secrets, { environment, secretPath })
// );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment: updatedEnvironment, secretPath: updatedSecretPath })
);
const newFolder = await folderDAL.findBySecretPath(
secretRotation.projectId,
updatedEnvironment,
updatedSecretPath
);
if (!newFolder)
throw new BadRequestError({
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${secretRotation.projectId}"`
});
folderId = newFolder.id;
}
try {
const updatedSecretRotation = await secretRotationV2DAL.updateById(rotationId, {
...params,
folderId
});
return updatedSecretRotation as TSecretRotationV2;
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({
message: `A Secret Rotation with the name "${params.name}" already exists for the project with ID "${secretRotation.projectId}"`
});
}
throw err;
}
};
const deleteSecretRotation = async (
{ type, rotationId, removeSecrets }: TDeleteSecretRotationV2DTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretRotation)
throw new BadRequestError({
message: "Failed to access secret rotation due to plan restriction. Upgrade plan to access secret rotations."
});
const secretRotation = await secretRotationV2DAL.findById(rotationId);
if (!secretRotation)
throw new NotFoundError({
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
});
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretManager,
projectId: secretRotation.projectId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretRotationActions.Delete,
ProjectPermissionSub.SecretRotation
);
if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
throw new BadRequestError({
message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
});
if (removeSecrets) {
// TODO: get secrets to determine remove permissions
// ForbiddenError.from(permission).throwUnlessCan(
// ProjectPermissionSecretRotationActions.RemoveSecrets,
// ProjectPermissionSub.SecretRotations
// );
// TODO: remove secrets
} else {
// TODO delete relations
}
await secretRotationV2DAL.deleteById(rotationId);
return secretRotation as TSecretRotationV2;
};
//
// const triggerSecretRotationRotationSecretsById = async (
// { rotationId, type, ...params }: TTriggerSecretRotationRotationSecretsByIdDTO,
// actor: OrgServiceActor
// ) => {
// const secretRotation = await secretRotationDAL.findById(rotationId);
//
// if (!secretRotation)
// throw new NotFoundError({
// message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
// });
//
// const { permission } = await permissionService.getProjectPermission({
// actor: actor.type,
// actorId: actor.id,
// actorAuthMethod: actor.authMethod,
// actorOrgId: actor.orgId,
// actionProjectType: ActionProjectType.SecretManager,
// projectId: secretRotation.projectId
// });
//
// ForbiddenError.from(permission).throwUnlessCan(
// ProjectPermissionSecretRotationActions.RotationSecrets,
// ProjectPermissionSub.SecretRotations
// );
//
// if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
// throw new BadRequestError({
// message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
// });
//
// if (!secretRotation.folderId)
// throw new BadRequestError({
// message: `Invalid source configuration: folder no longer exists. Please configure a valid source and try again.`
// });
//
// const isRotationJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretRotationLock(rotationId)));
//
// if (isRotationJobRunning)
// throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` });
//
// await secretRotationQueue.queueSecretRotationRotationSecretsById({ rotationId, ...params });
//
// const updatedSecretRotation = await secretRotationDAL.updateById(rotationId, {
// syncStatus: SecretRotationStatus.Pending
// });
//
// return updatedSecretRotation as TSecretRotation;
// };
//
// const triggerSecretRotationImportSecretsById = async (
// { rotationId, type, ...params }: TTriggerSecretRotationImportSecretsByIdDTO,
// actor: OrgServiceActor
// ) => {
// if (!listSecretRotationOptions().find((option) => option.type === type)?.canImportSecrets) {
// throw new BadRequestError({
// message: `${SECRET_ROTATION_NAME_MAP[type]} does not support importing secrets.`
// });
// }
//
// const secretRotation = await secretRotationDAL.findById(rotationId);
//
// if (!secretRotation)
// throw new NotFoundError({
// message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
// });
//
// const { permission } = await permissionService.getProjectPermission({
// actor: actor.type,
// actorId: actor.id,
// actorAuthMethod: actor.authMethod,
// actorOrgId: actor.orgId,
// actionProjectType: ActionProjectType.SecretManager,
// projectId: secretRotation.projectId
// });
//
// ForbiddenError.from(permission).throwUnlessCan(
// ProjectPermissionSecretRotationActions.ImportSecrets,
// ProjectPermissionSub.SecretRotations
// );
//
// if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
// throw new BadRequestError({
// message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
// });
//
// if (!secretRotation.folderId)
// throw new BadRequestError({
// message: `Invalid source configuration: folder no longer exists. Please configure a valid source and try again.`
// });
//
// const isRotationJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretRotationLock(rotationId)));
//
// if (isRotationJobRunning)
// throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` });
//
// await secretRotationQueue.queueSecretRotationImportSecretsById({ rotationId, ...params });
//
// const updatedSecretRotation = await secretRotationDAL.updateById(rotationId, {
// importStatus: SecretRotationStatus.Pending
// });
//
// return updatedSecretRotation as TSecretRotation;
// };
//
// const triggerSecretRotationRemoveSecretsById = async (
// { rotationId, type, ...params }: TTriggerSecretRotationRemoveSecretsByIdDTO,
// actor: OrgServiceActor
// ) => {
// const secretRotation = await secretRotationDAL.findById(rotationId);
//
// if (!secretRotation)
// throw new NotFoundError({
// message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
// });
//
// const { permission } = await permissionService.getProjectPermission({
// actor: actor.type,
// actorId: actor.id,
// actorAuthMethod: actor.authMethod,
// actorOrgId: actor.orgId,
// actionProjectType: ActionProjectType.SecretManager,
// projectId: secretRotation.projectId
// });
//
// ForbiddenError.from(permission).throwUnlessCan(
// ProjectPermissionSecretRotationActions.RemoveSecrets,
// ProjectPermissionSub.SecretRotations
// );
//
// if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
// throw new BadRequestError({
// message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
// });
//
// if (!secretRotation.folderId)
// throw new BadRequestError({
// message: `Invalid source configuration: folder no longer exists. Please configure a valid source and try again.`
// });
//
// const isRotationJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretRotationLock(rotationId)));
//
// if (isRotationJobRunning)
// throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` });
//
// await secretRotationQueue.queueSecretRotationRemoveSecretsById({ rotationId, ...params });
//
// const updatedSecretRotation = await secretRotationDAL.updateById(rotationId, {
// removeStatus: SecretRotationStatus.Pending
// });
//
// return updatedSecretRotation as TSecretRotation;
// };
return {
listSecretRotationOptions,
listSecretRotationsByProjectId,
createSecretRotation,
updateSecretRotation,
findSecretRotationById,
findSecretRotationByName,
deleteSecretRotation
// triggerSecretRotationRotationSecretsById,
// triggerSecretRotationImportSecretsById,
// triggerSecretRotationRemoveSecretsById
};
};

View File

@ -0,0 +1,52 @@
import {
TPostgresCredentialsRotation,
TPostgresCredentialsRotationInput,
TPostgresCredentialsRotationListItem,
TPostgresCredentialsRotationWithConnection
} from "./postgres-credentials";
import { SecretRotation } from "./secret-rotation-v2-enums";
export type TSecretRotationV2 = TPostgresCredentialsRotation;
export type TSecretRotationV2WithConnection = TPostgresCredentialsRotationWithConnection;
export type TSecretRotationV2Input = TPostgresCredentialsRotationInput;
export type TSecretRotationV2ListItem = TPostgresCredentialsRotationListItem;
export type TListSecretRotationsV2ByProjectId = {
projectId: string;
type?: SecretRotation;
};
export type TFindSecretRotationV2ByIdDTO = {
rotationId: string;
type: SecretRotation;
};
export type TFindSecretRotationV2ByNameDTO = {
rotationName: string;
projectId: string;
type: SecretRotation;
};
export type TCreateSecretRotationV2DTO = Pick<
TSecretRotationV2,
"parameters" | "description" | "interval" | "name" | "connectionId" | "projectId"
> & {
type: SecretRotation;
secretPath: string;
environment: string;
isAutoRotationEnabled?: boolean;
};
export type TUpdateSecretRotationV2DTO = Partial<Omit<TCreateSecretRotationV2DTO, "projectId" | "connectionId">> & {
rotationId: string;
type: SecretRotation;
};
export type TDeleteSecretRotationV2DTO = {
type: SecretRotation;
rotationId: string;
removeSecrets: boolean;
};

View File

@ -1,3 +1,8 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
SECRET_ROTATION_CONNECTION_MAP,
SECRET_ROTATION_NAME_MAP
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
@ -1649,7 +1654,8 @@ export const AppConnections = {
name: `The name of the ${appName} Connection to create. Must be slug-friendly.`,
description: `An optional description for the ${appName} Connection.`,
credentials: `The credentials used to connect with ${appName}.`,
method: `The method used to authenticate with ${appName}.`
method: `The method used to authenticate with ${appName}.`,
isPlatformManaged: `Whether or not the ${appName} Connection should be managed by Infisical.`
};
},
UPDATE: (app: AppConnection) => {
@ -1659,7 +1665,8 @@ export const AppConnections = {
name: `The updated name of the ${appName} Connection. Must be slug-friendly.`,
description: `The updated description of the ${appName} Connection.`,
credentials: `The credentials used to connect with ${appName}.`,
method: `The method used to authenticate with ${appName}.`
method: `The method used to authenticate with ${appName}.`,
isPlatformManaged: `Whether or not the ${appName} Connection should be managed by Infisical.`
};
},
DELETE: (app: AppConnection) => ({
@ -1780,3 +1787,67 @@ export const SecretSyncs = {
}
}
};
export const SecretRotations = {
LIST: (type?: SecretRotation) => ({
projectId: `The ID of the project to list ${type ? SECRET_ROTATION_NAME_MAP[type] : "Secret"} Rotations from.`
}),
GET_BY_ID: (type: SecretRotation) => ({
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve.`
}),
GET_CREDENTIALS_BY_ID: (type: SecretRotation) => ({
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve the credentials for.`
}),
GET_BY_NAME: (type: SecretRotation) => ({
rotationName: `The name of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve.`,
projectId: `The ID of the project the ${SECRET_ROTATION_NAME_MAP[type]} Rotation is associated with.`
}),
CREATE: (type: SecretRotation) => {
const destinationName = SECRET_ROTATION_NAME_MAP[type];
return {
name: `The name of the ${destinationName} Rotation to create. Must be slug-friendly.`,
description: `An optional description for the ${destinationName} Rotation.`,
projectId: "The ID of the project to create the rotation in.",
environment: `The slug of the project environment to create the rotation in.`,
secretPath: `The folder path to of the project to create the rotation in.`,
connectionId: `The ID of the ${
APP_CONNECTION_NAME_MAP[SECRET_ROTATION_CONNECTION_MAP[type]]
} Connection to use for rotation.`,
isAutoRotationEnabled: `Whether secrets should be automatically rotated when the specified interval has elapsed.`,
interval: `The interval, in days, to automatically rotate secrets.`
};
},
UPDATE: (type: SecretRotation) => {
const typeName = SECRET_ROTATION_NAME_MAP[type];
return {
rotationId: `The ID of the ${typeName} Rotation to be updated.`,
name: `The updated name of the ${typeName} Rotation. Must be slug-friendly.`,
environment: `The updated slug of the project environment to move the rotation to.`,
secretPath: `The updated folder path to move the rotation to.`,
description: `The updated description of the ${typeName} Rotation.`,
isAutoRotationEnabled: `Whether secrets should be automatically rotated when the specified interval has elapsed.`,
interval: `The interval, in days, to automatically rotate secrets.`
};
},
DELETE: (type: SecretRotation) => ({
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to be deleted.`,
removeSecrets: `Whether the secrets belonging to this rotation should be deleted.`
}),
ROTATE: (type: SecretRotation) => ({
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to rotate credentials for.`
}),
PARAMETERS: {
POSTGRES_CREDENTIALS: {
usernameSecretKey: "The secret key that the generated username will be mapped to.",
passwordSecretKey: "The secret key that the generated password will be mapped to.",
issueStatement: "The SQL statement to generate the credentials on rotation.",
revokeStatement: "The SQL statement to revoke expired credentials on rotation."
},
MSSQL_CREDENTIALS: {
usernameSecretKey: "The secret key that the generated username will be mapped to.",
passwordSecretKey: "The secret key that the generated password will be mapped to.",
issueStatement: "The SQL statement to generate the credentials on rotation.",
revokeStatement: "The SQL statement to revoke expired credentials on rotation."
}
}
};

View File

@ -75,6 +75,8 @@ import { secretReplicationServiceFactory } from "@app/ee/services/secret-replica
import { secretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
import { secretRotationQueueFactory } from "@app/ee/services/secret-rotation/secret-rotation-queue";
import { secretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
import { secretRotationV2DALFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-dal";
import { secretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service";
import { gitAppDALFactory } from "@app/ee/services/secret-scanning/git-app-dal";
import { gitAppInstallSessionDALFactory } from "@app/ee/services/secret-scanning/git-app-install-session-dal";
import { secretScanningDALFactory } from "@app/ee/services/secret-scanning/secret-scanning-dal";
@ -401,6 +403,8 @@ export const registerRoutes = async (
const gatewayDAL = gatewayDALFactory(db);
const projectGatewayDAL = projectGatewayDALFactory(db);
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
const permissionService = permissionServiceFactory({
permissionDAL,
orgRoleDAL,
@ -1482,6 +1486,15 @@ export const registerRoutes = async (
permissionService
});
const secretRotationV2Service = secretRotationV2ServiceFactory({
secretRotationV2DAL,
permissionService,
appConnectionService,
folderDAL,
projectBotService,
licenseService
});
await superAdminService.initServerCfg();
// setup the communication with license key server
@ -1583,7 +1596,8 @@ export const registerRoutes = async (
secretSync: secretSyncService,
kmip: kmipService,
kmipOperation: kmipOperationService,
gateway: gatewayService
gateway: gatewayService,
secretRotationV2: secretRotationV2Service
});
const cronJobs: CronJob[] = [];

View File

@ -24,8 +24,14 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
method: I["method"];
credentials: I["credentials"];
description?: string | null;
isPlatformManaged?: boolean;
}>;
updateSchema: z.ZodType<{
name?: string;
credentials?: I["credentials"];
description?: string | null;
isPlatformManaged?: boolean;
}>;
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
sanitizedResponseSchema: z.ZodTypeAny;
}) => {
const appName = APP_CONNECTION_NAME_MAP[app];
@ -208,10 +214,10 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, method, credentials, description } = req.body;
const { name, method, credentials, description, isPlatformManaged } = req.body;
const appConnection = (await server.services.appConnection.createAppConnection(
{ name, method, app, credentials, description },
{ name, method, app, credentials, description, isPlatformManaged },
req.permission
)) as T;
@ -224,7 +230,8 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
name,
method,
app,
connectionId: appConnection.id
connectionId: appConnection.id,
isPlatformManaged
}
}
});
@ -251,11 +258,11 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, credentials, description } = req.body;
const { name, credentials, description, isPlatformManaged } = req.body;
const { connectionId } = req.params;
const appConnection = (await server.services.appConnection.updateAppConnection(
{ name, credentials, connectionId, description },
{ name, credentials, connectionId, description, isPlatformManaged },
req.permission
)) as T;
@ -268,7 +275,8 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
name,
description,
credentialsUpdated: Boolean(credentials),
connectionId
connectionId,
isPlatformManaged
}
}
});

View File

@ -22,6 +22,11 @@ import {
HumanitecConnectionListItemSchema,
SanitizedHumanitecConnectionSchema
} from "@app/services/app-connection/humanitec";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import {
PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema
} from "@app/services/app-connection/postgres";
import { AuthMode } from "@app/services/auth/auth-type";
// can't use discriminated due to multiple schemas for certain apps
@ -32,7 +37,9 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedAzureKeyVaultConnectionSchema.options,
...SanitizedAzureAppConfigurationConnectionSchema.options,
...SanitizedDatabricksConnectionSchema.options,
...SanitizedHumanitecConnectionSchema.options
...SanitizedHumanitecConnectionSchema.options,
...SanitizedPostgresConnectionSchema.options,
...SanitizedMsSqlConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@ -42,7 +49,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AzureKeyVaultConnectionListItemSchema,
AzureAppConfigurationConnectionListItemSchema,
DatabricksConnectionListItemSchema,
HumanitecConnectionListItemSchema
HumanitecConnectionListItemSchema,
PostgresConnectionListItemSchema,
MsSqlConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@ -7,6 +7,8 @@ import { registerDatabricksConnectionRouter } from "./databricks-connection-rout
import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
export * from "./app-connection-router";
@ -18,5 +20,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
[AppConnection.Humanitec]: registerHumanitecConnectionRouter
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
[AppConnection.Postgres]: registerPostgresConnectionRouter,
[AppConnection.MsSql]: registerMsSqlConnectionRouter
};

View File

@ -0,0 +1,18 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateMsSqlConnectionSchema,
SanitizedMsSqlConnectionSchema,
UpdateMsSqlConnectionSchema
} from "@app/services/app-connection/mssql";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerMsSqlConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.MsSql,
server,
sanitizedResponseSchema: SanitizedMsSqlConnectionSchema,
createSchema: CreateMsSqlConnectionSchema,
updateSchema: UpdateMsSqlConnectionSchema
});
};

View File

@ -0,0 +1,18 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreatePostgresConnectionSchema,
SanitizedPostgresConnectionSchema,
UpdatePostgresConnectionSchema
} from "@app/services/app-connection/postgres";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerPostgresConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Postgres,
server,
sanitizedResponseSchema: SanitizedPostgresConnectionSchema,
createSchema: CreatePostgresConnectionSchema,
updateSchema: UpdatePostgresConnectionSchema
});
};

View File

@ -5,7 +5,9 @@ export enum AppConnection {
GCP = "gcp",
AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration",
Humanitec = "humanitec"
Humanitec = "humanitec",
Postgres = "postgres",
MsSql = "mssql"
}
export enum AWSRegion {

View File

@ -1,30 +1,11 @@
import { TAppConnections } from "@app/db/schemas/app-connections";
import { generateHash } from "@app/lib/crypto/encryption";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
import {
AwsConnectionMethod,
getAwsConnectionListItem,
validateAwsConnectionCredentials
} from "@app/services/app-connection/aws";
import {
DatabricksConnectionMethod,
getDatabricksConnectionListItem,
validateDatabricksConnectionCredentials
} from "@app/services/app-connection/databricks";
import {
GcpConnectionMethod,
getGcpConnectionListItem,
validateGcpConnectionCredentials
} from "@app/services/app-connection/gcp";
import {
getGitHubConnectionListItem,
GitHubConnectionMethod,
validateGitHubConnectionCredentials
} from "@app/services/app-connection/github";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { AppConnection } from "./app-connection-enums";
import { TAppConnectionServiceFactoryDep } from "./app-connection-service";
import { TAppConnection, TAppConnectionConfig, TAppConnectionCredentialValidator } from "./app-connection-types";
import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws";
import {
AzureAppConfigurationConnectionMethod,
getAzureAppConfigurationConnectionListItem,
@ -35,11 +16,24 @@ import {
getAzureKeyVaultConnectionListItem,
validateAzureKeyVaultConnectionCredentials
} from "./azure-key-vault";
import {
DatabricksConnectionMethod,
getDatabricksConnectionListItem,
validateDatabricksConnectionCredentials
} from "./databricks";
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
import {
getHumanitecConnectionListItem,
HumanitecConnectionMethod,
validateHumanitecConnectionCredentials
} from "./humanitec";
import { getMsSqlConnectionListItem, MsSqlConnectionMethod, validateMsSqlConnectionCredentials } from "./mssql";
import {
getPostgresConnectionListItem,
PostgresConnectionMethod,
validatePostgresConnectionCredentials
} from "./postgres";
export const listAppConnectionOptions = () => {
return [
@ -49,7 +43,9 @@ export const listAppConnectionOptions = () => {
getAzureKeyVaultConnectionListItem(),
getAzureAppConfigurationConnectionListItem(),
getDatabricksConnectionListItem(),
getHumanitecConnectionListItem()
getHumanitecConnectionListItem(),
getPostgresConnectionListItem(),
getMsSqlConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@ -95,30 +91,22 @@ export const decryptAppConnectionCredentials = async ({
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
};
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialValidator> = {
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialValidator,
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialValidator,
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialValidator,
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialValidator,
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialValidator,
[AppConnection.AzureAppConfiguration]:
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialValidator,
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialValidator,
[AppConnection.Postgres]: validatePostgresConnectionCredentials as TAppConnectionCredentialValidator,
[AppConnection.MsSql]: validateMsSqlConnectionCredentials as TAppConnectionCredentialValidator
};
export const validateAppConnectionCredentials = async (
appConnection: TAppConnectionConfig
): Promise<TAppConnection["credentials"]> => {
const { app } = appConnection;
switch (app) {
case AppConnection.AWS:
return validateAwsConnectionCredentials(appConnection);
case AppConnection.Databricks:
return validateDatabricksConnectionCredentials(appConnection);
case AppConnection.GitHub:
return validateGitHubConnectionCredentials(appConnection);
case AppConnection.GCP:
return validateGcpConnectionCredentials(appConnection);
case AppConnection.AzureKeyVault:
return validateAzureKeyVaultConnectionCredentials(appConnection);
case AppConnection.AzureAppConfiguration:
return validateAzureAppConfigurationConnectionCredentials(appConnection);
case AppConnection.Humanitec:
return validateHumanitecConnectionCredentials(appConnection);
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection ${app}`);
}
};
): Promise<TAppConnection["credentials"]> => VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
switch (method) {
@ -136,8 +124,11 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "Service Account Impersonation";
case DatabricksConnectionMethod.ServicePrincipal:
return "Service Principal";
case HumanitecConnectionMethod.API_TOKEN:
case HumanitecConnectionMethod.ApiToken:
return "API Token";
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:
return "Username & Password";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);

View File

@ -7,5 +7,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.AzureKeyVault]: "Azure Key Vault",
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
[AppConnection.Databricks]: "Databricks",
[AppConnection.Humanitec]: "Humanitec"
[AppConnection.Humanitec]: "Humanitec",
[AppConnection.Postgres]: "PostgreSQL",
[AppConnection.MsSql]: "Microsoft SQL Server"
};

View File

@ -3,6 +3,8 @@ import { z } from "zod";
import { AppConnectionsSchema } from "@app/db/schemas/app-connections";
import { AppConnections } from "@app/lib/api-docs";
import { slugSchema } from "@app/server/lib/schemas";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { TAppConnectionBaseConfig } from "@app/services/app-connection/app-connection-types";
import { AppConnection } from "./app-connection-enums";
@ -14,7 +16,10 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
credentialsHash: z.string().optional()
});
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
export const GenericCreateAppConnectionFieldsSchema = (
app: AppConnection,
{ supportsPlatformManagement = false }: TAppConnectionBaseConfig = {}
) =>
z.object({
name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name),
description: z
@ -22,10 +27,16 @@ export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
.trim()
.max(256, "Description cannot exceed 256 characters")
.nullish()
.describe(AppConnections.CREATE(app).description)
.describe(AppConnections.CREATE(app).description),
isPlatformManaged: supportsPlatformManagement
? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManaged)
: z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
});
export const GenericUpdateAppConnectionFieldsSchema = (app: AppConnection) =>
export const GenericUpdateAppConnectionFieldsSchema = (
app: AppConnection,
{ supportsPlatformManagement = false }: TAppConnectionBaseConfig = {}
) =>
z.object({
name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(),
description: z
@ -33,5 +44,8 @@ export const GenericUpdateAppConnectionFieldsSchema = (app: AppConnection) =>
.trim()
.max(256, "Description cannot exceed 256 characters")
.nullish()
.describe(AppConnections.UPDATE(app).description)
.describe(AppConnections.UPDATE(app).description),
isPlatformManaged: supportsPlatformManagement
? z.boolean().optional().describe(AppConnections.CREATE(app).isPlatformManaged)
: z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
});

View File

@ -5,8 +5,8 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { generateHash } from "@app/lib/crypto/encryption";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
decryptAppConnection,
encryptAppConnectionCredentials,
@ -14,17 +14,18 @@ import {
listAppConnectionOptions,
validateAppConnectionCredentials
} from "@app/services/app-connection/app-connection-fns";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "./app-connection-dal";
import { AppConnection } from "./app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "./app-connection-maps";
import {
TAppConnection,
TAppConnectionConfig,
TCreateAppConnectionDTO,
TUpdateAppConnectionDTO,
TValidateAppConnectionCredentials
} from "@app/services/app-connection/app-connection-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "./app-connection-dal";
} from "./app-connection-types";
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
import { awsConnectionService } from "./aws/aws-connection-service";
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
@ -37,6 +38,8 @@ import { ValidateGitHubConnectionCredentialsSchema } from "./github";
import { githubConnectionService } from "./github/github-connection-service";
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
export type TAppConnectionServiceFactoryDep = {
appConnectionDAL: TAppConnectionDALFactory;
@ -53,7 +56,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
@ -160,7 +165,8 @@ export const appConnectionServiceFactory = ({
app,
credentials,
method,
orgId: actor.orgId
orgId: actor.orgId,
isPlatformManaged: params.isPlatformManaged
} as TAppConnectionConfig);
const encryptedCredentials = await encryptAppConnectionCredentials({
@ -213,6 +219,13 @@ export const appConnectionServiceFactory = ({
OrgPermissionSubjects.AppConnections
);
// prevent updating credentials or management status if platform managed
if (appConnection.isPlatformManaged && (params.isPlatformManaged === false || credentials)) {
throw new BadRequestError({
message: "Cannot update credentials or management status for platform managed connections"
});
}
let encryptedCredentials: undefined | Buffer;
if (credentials) {
@ -230,11 +243,14 @@ export const appConnectionServiceFactory = ({
} Connection with method ${getAppConnectionMethodName(method)}`
});
logger.warn(params, "PARAMS");
const validatedCredentials = await validateAppConnectionCredentials({
app,
orgId: actor.orgId,
credentials,
method
method,
isPlatformManaged: params.isPlatformManaged
} as TAppConnectionConfig);
if (!validatedCredentials)

View File

@ -1,24 +1,7 @@
import { AWSRegion } from "@app/services/app-connection/app-connection-enums";
import {
TAwsConnection,
TAwsConnectionConfig,
TAwsConnectionInput,
TValidateAwsConnectionCredentials
} from "@app/services/app-connection/aws";
import {
TDatabricksConnection,
TDatabricksConnectionConfig,
TDatabricksConnectionInput,
TValidateDatabricksConnectionCredentials
} from "@app/services/app-connection/databricks";
import {
TGitHubConnection,
TGitHubConnectionConfig,
TGitHubConnectionInput,
TValidateGitHubConnectionCredentials
} from "@app/services/app-connection/github";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { AWSRegion } from "./app-connection-enums";
import { TAwsConnection, TAwsConnectionConfig, TAwsConnectionInput, TValidateAwsConnectionCredentials } from "./aws";
import {
TAzureAppConfigurationConnection,
TAzureAppConfigurationConnectionConfig,
@ -31,13 +14,37 @@ import {
TAzureKeyVaultConnectionInput,
TValidateAzureKeyVaultConnectionCredentials
} from "./azure-key-vault";
import {
TDatabricksConnection,
TDatabricksConnectionConfig,
TDatabricksConnectionInput,
TValidateDatabricksConnectionCredentials
} from "./databricks";
import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp";
import {
TGitHubConnection,
TGitHubConnectionConfig,
TGitHubConnectionInput,
TValidateGitHubConnectionCredentials
} from "./github";
import {
THumanitecConnection,
THumanitecConnectionConfig,
THumanitecConnectionInput,
TValidateHumanitecConnectionCredentials
} from "./humanitec";
import {
TMsSqlConnection,
TMsSqlConnectionConfig,
TMsSqlConnectionInput,
TValidateMsSqlConnectionCredentials
} from "./mssql";
import {
TPostgresConnection,
TPostgresConnectionConfig,
TPostgresConnectionInput,
TValidatePostgresConnectionCredentials
} from "./postgres";
export type TAppConnection = { id: string } & (
| TAwsConnection
@ -47,6 +54,8 @@ export type TAppConnection = { id: string } & (
| TAzureAppConfigurationConnection
| TDatabricksConnection
| THumanitecConnection
| TPostgresConnection
| TMsSqlConnection
);
export type TAppConnectionInput = { id: string } & (
@ -57,11 +66,13 @@ export type TAppConnectionInput = { id: string } & (
| TAzureAppConfigurationConnectionInput
| TDatabricksConnectionInput
| THumanitecConnectionInput
| TPostgresConnectionInput
| TMsSqlConnectionInput
);
export type TCreateAppConnectionDTO = Pick<
TAppConnectionInput,
"credentials" | "method" | "name" | "app" | "description"
"credentials" | "method" | "name" | "app" | "description" | "isPlatformManaged"
>;
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
@ -75,7 +86,9 @@ export type TAppConnectionConfig =
| TAzureKeyVaultConnectionConfig
| TAzureAppConfigurationConnectionConfig
| TDatabricksConnectionConfig
| THumanitecConnectionConfig;
| THumanitecConnectionConfig
| TPostgresConnectionConfig
| TMsSqlConnectionConfig;
export type TValidateAppConnectionCredentials =
| TValidateAwsConnectionCredentials
@ -84,10 +97,20 @@ export type TValidateAppConnectionCredentials =
| TValidateAzureKeyVaultConnectionCredentials
| TValidateAzureAppConfigurationConnectionCredentials
| TValidateDatabricksConnectionCredentials
| TValidateHumanitecConnectionCredentials;
| TValidateHumanitecConnectionCredentials
| TValidatePostgresConnectionCredentials
| TValidateMsSqlConnectionCredentials;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
region: AWSRegion;
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
};
export type TAppConnectionCredentialValidator = (
appConnection: TAppConnectionConfig
) => Promise<TAppConnection["credentials"]>;
export type TAppConnectionBaseConfig = {
supportsPlatformManagement?: boolean;
};

View File

@ -48,11 +48,11 @@ export const SanitizedAwsConnectionSchema = z.discriminatedUnion("method", [
export const ValidateAwsConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(AwsConnectionMethod.AssumeRole).describe(AppConnections?.CREATE(AppConnection.AWS).method),
method: z.literal(AwsConnectionMethod.AssumeRole).describe(AppConnections.CREATE(AppConnection.AWS).method),
credentials: AwsConnectionAssumeRoleCredentialsSchema.describe(AppConnections.CREATE(AppConnection.AWS).credentials)
}),
z.object({
method: z.literal(AwsConnectionMethod.AccessKey).describe(AppConnections?.CREATE(AppConnection.AWS).method),
method: z.literal(AwsConnectionMethod.AccessKey).describe(AppConnections.CREATE(AppConnection.AWS).method),
credentials: AwsConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.AWS).credentials
)

View File

@ -49,7 +49,7 @@ export const ValidateDatabricksConnectionCredentialsSchema = z.discriminatedUnio
z.object({
method: z
.literal(DatabricksConnectionMethod.ServicePrincipal)
.describe(AppConnections?.CREATE(AppConnection.Databricks).method),
.describe(AppConnections.CREATE(AppConnection.Databricks).method),
credentials: DatabricksConnectionServicePrincipalInputCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Databricks).credentials
)

View File

@ -4,10 +4,10 @@ import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums";
import { getAppConnectionMethodName } from "../app-connection-fns";
import { GcpConnectionMethod } from "./gcp-connection-enums";
import {
GCPApp,

View File

@ -37,7 +37,7 @@ export const ValidateGcpConnectionCredentialsSchema = z.discriminatedUnion("meth
z.object({
method: z
.literal(GcpConnectionMethod.ServiceAccountImpersonation)
.describe(AppConnections?.CREATE(AppConnection.GCP).method),
.describe(AppConnections.CREATE(AppConnection.GCP).method),
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.GCP).credentials
)

View File

@ -1,3 +1,3 @@
export enum HumanitecConnectionMethod {
API_TOKEN = "api-token"
ApiToken = "api-token"
}

View File

@ -18,7 +18,7 @@ export const getHumanitecConnectionListItem = () => {
return {
name: "Humanitec" as const,
app: AppConnection.Humanitec as const,
methods: Object.values(HumanitecConnectionMethod) as [HumanitecConnectionMethod.API_TOKEN]
methods: Object.values(HumanitecConnectionMethod) as [HumanitecConnectionMethod.ApiToken]
};
};

View File

@ -17,13 +17,13 @@ export const HumanitecConnectionAccessTokenCredentialsSchema = z.object({
const BaseHumanitecConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Humanitec) });
export const HumanitecConnectionSchema = BaseHumanitecConnectionSchema.extend({
method: z.literal(HumanitecConnectionMethod.API_TOKEN),
method: z.literal(HumanitecConnectionMethod.ApiToken),
credentials: HumanitecConnectionAccessTokenCredentialsSchema
});
export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method", [
BaseHumanitecConnectionSchema.extend({
method: z.literal(HumanitecConnectionMethod.API_TOKEN),
method: z.literal(HumanitecConnectionMethod.ApiToken),
credentials: HumanitecConnectionAccessTokenCredentialsSchema.pick({})
})
]);
@ -31,8 +31,8 @@ export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method",
export const ValidateHumanitecConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(HumanitecConnectionMethod.API_TOKEN)
.describe(AppConnections?.CREATE(AppConnection.Humanitec).method),
.literal(HumanitecConnectionMethod.ApiToken)
.describe(AppConnections.CREATE(AppConnection.Humanitec).method),
credentials: HumanitecConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Humanitec).credentials
)

View File

@ -0,0 +1,4 @@
export * from "./mssql-connection-enums";
export * from "./mssql-connection-fns";
export * from "./mssql-connection-schemas";
export * from "./mssql-connection-types";

View File

@ -0,0 +1,3 @@
export enum MsSqlConnectionMethod {
UsernameAndPassword = "username-and-password"
}

View File

@ -0,0 +1,53 @@
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { sqlConnectionQuery } from "@app/services/app-connection/shared/sql";
import { MsSqlConnectionMethod } from "./mssql-connection-enums";
import { TMsSqlConnectionConfig } from "./mssql-connection-types";
export const getMsSqlConnectionListItem = () => {
return {
name: "Microsoft SQL Server" as const,
app: AppConnection.MsSql as const,
methods: Object.values(MsSqlConnectionMethod) as [MsSqlConnectionMethod.UsernameAndPassword],
supportsPlatformManagement: true as const
};
};
export const validateMsSqlConnectionCredentials = async (config: TMsSqlConnectionConfig) => {
const { credentials, isPlatformManaged } = config;
try {
if (isPlatformManaged) {
const newPassword = alphaNumericNanoId(32);
await sqlConnectionQuery({
credentials,
app: AppConnection.MsSql,
query: `ALTER LOGIN ?? WITH PASSWORD = '${newPassword}' OLD_PASSWORD = '${credentials.password}';`,
variables: [credentials.username]
});
return {
...credentials,
password: newPassword
};
}
await sqlConnectionQuery({
credentials,
app: AppConnection.MsSql,
query: "SELECT GETDATE()"
});
return credentials;
} catch (e) {
if ((e as { number: number }).number === 15151) {
throw new BadRequestError({
message: `Cannot alter the login '${credentials.username}', because it does not exist or you do not have permission.`
});
}
throw new BadRequestError({ message: "Unable to validate connection - verify credentials" });
}
};

View File

@ -0,0 +1,65 @@
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 { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql";
import { MsSqlConnectionMethod } from "./mssql-connection-enums";
export const MsSqlConnectionAccessTokenCredentialsSchema = BaseSqlUsernameAndPasswordConnectionSchema;
const BaseMsSqlConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.MsSql)
});
export const MsSqlConnectionSchema = BaseMsSqlConnectionSchema.extend({
method: z.literal(MsSqlConnectionMethod.UsernameAndPassword),
credentials: MsSqlConnectionAccessTokenCredentialsSchema
});
export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [
BaseMsSqlConnectionSchema.extend({
method: z.literal(MsSqlConnectionMethod.UsernameAndPassword),
credentials: MsSqlConnectionAccessTokenCredentialsSchema.pick({
host: true,
database: true,
port: true,
username: true
})
})
]);
export const ValidateMsSqlConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(MsSqlConnectionMethod.UsernameAndPassword)
.describe(AppConnections.CREATE(AppConnection.MsSql).method),
credentials: MsSqlConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.MsSql).credentials
)
})
]);
export const CreateMsSqlConnectionSchema = ValidateMsSqlConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagement: true })
);
export const UpdateMsSqlConnectionSchema = z
.object({
credentials: MsSqlConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.MsSql).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagement: true }));
export const MsSqlConnectionListItemSchema = z.object({
name: z.literal("Microsoft SQL Server"),
app: z.literal(AppConnection.MsSql),
methods: z.nativeEnum(MsSqlConnectionMethod).array(),
supportsPlatformManagement: z.literal(true)
});

View File

@ -0,0 +1,25 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateMsSqlConnectionSchema,
MsSqlConnectionSchema,
ValidateMsSqlConnectionCredentialsSchema
} from "./mssql-connection-schemas";
export type TMsSqlConnection = z.infer<typeof MsSqlConnectionSchema>;
export type TMsSqlConnectionInput = z.infer<typeof CreateMsSqlConnectionSchema> & {
app: AppConnection.MsSql;
};
export type TValidateMsSqlConnectionCredentials = typeof ValidateMsSqlConnectionCredentialsSchema;
export type TMsSqlConnectionConfig = DiscriminativePick<
TMsSqlConnectionInput,
"method" | "app" | "credentials" | "isPlatformManaged"
> & {
orgId: string;
};

View File

@ -0,0 +1,4 @@
export * from "./postgres-connection-enums";
export * from "./postgres-connection-fns";
export * from "./postgres-connection-schemas";
export * from "./postgres-connection-types";

View File

@ -0,0 +1,3 @@
export enum PostgresConnectionMethod {
UsernameAndPassword = "username-and-password"
}

View File

@ -0,0 +1,47 @@
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { sqlConnectionQuery } from "@app/services/app-connection/shared/sql";
import { PostgresConnectionMethod } from "./postgres-connection-enums";
import { TPostgresConnectionConfig } from "./postgres-connection-types";
export const getPostgresConnectionListItem = () => {
return {
name: "PostgreSQL" as const,
app: AppConnection.Postgres as const,
methods: Object.values(PostgresConnectionMethod) as [PostgresConnectionMethod.UsernameAndPassword],
supportsPlatformManagement: true as const
};
};
export const validatePostgresConnectionCredentials = async (config: TPostgresConnectionConfig) => {
const { credentials, isPlatformManaged } = config;
try {
if (isPlatformManaged) {
const newPassword = alphaNumericNanoId(32);
await sqlConnectionQuery({
credentials,
app: AppConnection.Postgres,
query: `ALTER ROLE ?? WITH PASSWORD '${newPassword}';`,
variables: [credentials.username]
});
return {
...credentials,
password: newPassword
};
}
await sqlConnectionQuery({
credentials,
app: AppConnection.Postgres,
query: "SELECT NOW()"
});
return credentials;
} catch (e) {
throw new BadRequestError({ message: "Unable to validate connection - verify credentials" });
}
};

View File

@ -0,0 +1,63 @@
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 { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql";
import { PostgresConnectionMethod } from "./postgres-connection-enums";
export const PostgresConnectionAccessTokenCredentialsSchema = BaseSqlUsernameAndPasswordConnectionSchema;
const BasePostgresConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Postgres) });
export const PostgresConnectionSchema = BasePostgresConnectionSchema.extend({
method: z.literal(PostgresConnectionMethod.UsernameAndPassword),
credentials: PostgresConnectionAccessTokenCredentialsSchema
});
export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method", [
BasePostgresConnectionSchema.extend({
method: z.literal(PostgresConnectionMethod.UsernameAndPassword),
credentials: PostgresConnectionAccessTokenCredentialsSchema.pick({
host: true,
database: true,
port: true,
username: true
})
})
]);
export const ValidatePostgresConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(PostgresConnectionMethod.UsernameAndPassword)
.describe(AppConnections.CREATE(AppConnection.Postgres).method),
credentials: PostgresConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Postgres).credentials
)
})
]);
export const CreatePostgresConnectionSchema = ValidatePostgresConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagement: true })
);
export const UpdatePostgresConnectionSchema = z
.object({
credentials: PostgresConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Postgres).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagement: true }));
export const PostgresConnectionListItemSchema = z.object({
name: z.literal("PostgreSQL"),
app: z.literal(AppConnection.Postgres),
methods: z.nativeEnum(PostgresConnectionMethod).array(),
supportsPlatformManagement: z.literal(true)
});

View File

@ -0,0 +1,25 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreatePostgresConnectionSchema,
PostgresConnectionSchema,
ValidatePostgresConnectionCredentialsSchema
} from "./postgres-connection-schemas";
export type TPostgresConnection = z.infer<typeof PostgresConnectionSchema>;
export type TPostgresConnectionInput = z.infer<typeof CreatePostgresConnectionSchema> & {
app: AppConnection.Postgres;
};
export type TValidatePostgresConnectionCredentials = typeof ValidatePostgresConnectionCredentialsSchema;
export type TPostgresConnectionConfig = DiscriminativePick<
TPostgresConnectionInput,
"method" | "app" | "credentials" | "isPlatformManaged"
> & {
orgId: string;
};

View File

@ -0,0 +1,2 @@
export * from "./sql-connection-fns";
export * from "./sql-connection-schemas";

View File

@ -0,0 +1,62 @@
import knex from "knex";
import { getConfig } from "@app/lib/config/env";
import { getDbConnectionHost } from "@app/lib/knex";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TSqlConnectionQueryParams } from "./sql-connection-types";
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
const SQL_CONNECTION_CLIENT_MAP = {
[AppConnection.Postgres]: "pg",
[AppConnection.MsSql]: "mssql"
};
export const sqlConnectionQuery = async ({
credentials: { host, database, port, ca, password, username },
app,
query,
variables = [],
options
}: TSqlConnectionQueryParams) => {
const appCfg = getConfig();
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
if (
(isCloud &&
// internal ips
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))) ||
host === "localhost" ||
host === "127.0.0.1" ||
// Infisical's database
dbHost === host
)
throw new Error("Invalid Host");
const db = knex({
client: SQL_CONNECTION_CLIENT_MAP[app],
connection: {
database,
port,
host,
user: username,
password,
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
ssl,
pool: { min: 0, max: 1 },
options
}
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const results = await db.raw(query, variables);
await db.destroy();
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return results;
};

View File

@ -0,0 +1,10 @@
import { z } from "zod";
export const BaseSqlUsernameAndPasswordConnectionSchema = z.object({
host: z.string().trim().min(1, "Host required"),
port: z.coerce.number(),
database: z.string().trim().min(1, "Database required"),
username: z.string().trim().min(1, "Username required"),
password: z.string().trim().min(1, "Password required"),
ca: z.string().trim().optional()
});

View File

@ -0,0 +1,15 @@
import z from "zod";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { BaseSqlUsernameAndPasswordConnectionSchema } from "./sql-connection-schemas";
export type TBaseSqlConnectionCredentialsSchema = z.infer<typeof BaseSqlUsernameAndPasswordConnectionSchema>;
export type TSqlConnectionQueryParams = {
credentials: TBaseSqlConnectionCredentialsSchema;
app: AppConnection.Postgres | AppConnection.MsSql;
query: string;
variables?: unknown[];
options?: Record<string, unknown>;
};

View File

@ -31,7 +31,8 @@ const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: Secre
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt")
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
db.ref("isPlatformManaged").withSchema(TableName.AppConnection).as("connectionIsPlatformManaged")
);
if (filter) {
@ -60,6 +61,7 @@ const expandSecretSync = (
connectionCreatedAt,
connectionUpdatedAt,
connectionVersion,
connectionIsPlatformManaged,
...el
} = secretSync;
@ -77,7 +79,8 @@ const expandSecretSync = (
description: connectionDescription,
createdAt: connectionCreatedAt,
updatedAt: connectionUpdatedAt,
version: connectionVersion
version: connectionVersion,
isPlatformManaged: connectionIsPlatformManaged
},
folder: folder
? {
@ -166,14 +169,14 @@ export const secretSyncDALFactory = (
}
};
const find = async (filter: Parameters<(typeof secretSyncOrm)["find"]>[0], tx?: Knex) => {
const find = async (filter: Parameters<(typeof secretSyncOrm)["find"]>[0] & { projectId: string }, tx?: Knex) => {
try {
const secretSyncs = await baseSecretSyncQuery({ filter, db, tx });
if (!secretSyncs.length) return [];
const foldersWithPath = await folderDAL.findSecretPathByFolderIds(
secretSyncs[0].projectId,
filter.projectId,
secretSyncs.filter((sync) => Boolean(sync.folderId)).map((sync) => sync.folderId!)
);

View File

@ -119,14 +119,10 @@ export const secretSyncServiceFactory = ({
{ destination, syncName, projectId }: TFindSecretSyncByNameDTO,
actor: OrgServiceActor
) => {
const folders = await folderDAL.findByProjectId(projectId);
// we prevent conflicting names within a project so this will only return one at most
const [secretSync] = await secretSyncDAL.find({
// we prevent conflicting names within a project
const secretSync = await secretSyncDAL.findOne({
name: syncName,
$in: {
folderId: folders.map((folder) => folder.id)
}
projectId
});
if (!secretSync)

View File

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

View File

@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/mssql"
---

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/mssql/{connectionId}"
---

View File

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

View File

@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/postgres"
---

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/postgres/{connectionId}"
---

View File

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

View File

@ -0,0 +1,4 @@
---
title: "Options"
openapi: "GET /api/v2/secret-rotations/options"
---

View File

@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v2/secret-rotations/postgres-credentials"
---

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
---
title: "Rotate"
openapi: "POST /api/v2/secret-rotations/postgres-credentials/{rotationId}/rotate"
---

View File

@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v2/secret-rotations/postgres-credentials/{rotationId}"
---

View File

@ -1,6 +1,6 @@
{
"name": "Infisical",
"openapi": "https://app.infisical.com/api/docs/json",
"openapi": "https://bubblegloop-swamp.ngrok.dev/api/docs/json",
"logo": {
"dark": "/logo/dark.svg",
"light": "/logo/light.svg",
@ -813,6 +813,26 @@
"api-reference/endpoints/secret-imports/delete"
]
},
{
"group": "Secret Rotations",
"pages": [
"api-reference/endpoints/secret-rotations/list",
"api-reference/endpoints/secret-rotations/options",
{
"group": "PostgreSQL Credentials",
"pages": [
"api-reference/endpoints/secret-rotations/postgres-credentials/create",
"api-reference/endpoints/secret-rotations/postgres-credentials/delete",
"api-reference/endpoints/secret-rotations/postgres-credentials/get-by-id",
"api-reference/endpoints/secret-rotations/postgres-credentials/get-by-name",
"api-reference/endpoints/secret-rotations/postgres-credentials/get-credentials-by-id",
"api-reference/endpoints/secret-rotations/postgres-credentials/list",
"api-reference/endpoints/secret-rotations/postgres-credentials/rotate",
"api-reference/endpoints/secret-rotations/postgres-credentials/update"
]
}
]
},
{
"group": "Identity Specific Privilege",
"pages": [
@ -912,6 +932,30 @@
"api-reference/endpoints/app-connections/humanitec/update",
"api-reference/endpoints/app-connections/humanitec/delete"
]
},
{
"group": "Microsoft SQL Server",
"pages": [
"api-reference/endpoints/app-connections/mssql/list",
"api-reference/endpoints/app-connections/mssql/available",
"api-reference/endpoints/app-connections/mssql/get-by-id",
"api-reference/endpoints/app-connections/mssql/get-by-name",
"api-reference/endpoints/app-connections/mssql/create",
"api-reference/endpoints/app-connections/mssql/update",
"api-reference/endpoints/app-connections/mssql/delete"
]
},
{
"group": "PostgreSQL",
"pages": [
"api-reference/endpoints/app-connections/postgres/list",
"api-reference/endpoints/app-connections/postgres/available",
"api-reference/endpoints/app-connections/postgres/get-by-id",
"api-reference/endpoints/app-connections/postgres/get-by-name",
"api-reference/endpoints/app-connections/postgres/create",
"api-reference/endpoints/app-connections/postgres/update",
"api-reference/endpoints/app-connections/postgres/delete"
]
}
]
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -1,5 +1,5 @@
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import { faKey, faPassport, faUser } from "@fortawesome/free-solid-svg-icons";
import { faKey, faLock, faPassport, faUser } from "@fortawesome/free-solid-svg-icons";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
@ -8,10 +8,12 @@ import {
AzureKeyVaultConnectionMethod,
GcpConnectionMethod,
GitHubConnectionMethod,
PostgresConnectionMethod,
TAppConnection
} from "@app/hooks/api/appConnections/types";
import { DatabricksConnectionMethod } from "@app/hooks/api/appConnections/types/databricks-connection";
import { HumanitecConnectionMethod } from "@app/hooks/api/appConnections/types/humanitec-connection";
import { MsSqlConnectionMethod } from "@app/hooks/api/appConnections/types/mssql-connection";
export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: string }> = {
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
@ -26,7 +28,9 @@ export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: st
image: "Microsoft Azure.png"
},
[AppConnection.Databricks]: { name: "Databricks", image: "Databricks.png" },
[AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" }
[AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" },
[AppConnection.Postgres]: { name: "PostgreSQL", image: "Postgres.png" },
[AppConnection.MsSql]: { name: "Microsoft SQL Server", image: "MsSql.png" }
};
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
@ -45,8 +49,11 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
return { name: "Service Account Impersonation", icon: faUser };
case DatabricksConnectionMethod.ServicePrincipal:
return { name: "Service Principal", icon: faUser };
case HumanitecConnectionMethod.API_TOKEN:
case HumanitecConnectionMethod.ApiToken:
return { name: "API Token", icon: faKey };
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:
return { name: "Username & Password", icon: faLock };
default:
throw new Error(`Unhandled App Connection Method: ${method}`);
}

View File

@ -1,6 +1,7 @@
import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { Identity } from "@app/hooks/api/identities/types";
import { User } from "../types";
import {
@ -10,7 +11,6 @@ import {
TGetServerRootKmsEncryptionDetails,
TServerConfig
} from "./types";
import { Identity } from "@app/hooks/api/identities/types";
export const adminStandaloneKeys = {
getUsers: "get-users",

View File

@ -5,5 +5,7 @@ export enum AppConnection {
AzureKeyVault = "azure-key-vault",
AzureAppConfiguration = "azure-app-configuration",
Databricks = "databricks",
Humanitec = "humanitec"
Humanitec = "humanitec",
Postgres = "postgres",
MsSql = "mssql"
}

View File

@ -3,6 +3,7 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
export type TAppConnectionOptionBase = {
name: string;
methods: string[];
supportsPlatformManagement?: boolean;
};
export type TAwsConnectionOption = TAppConnectionOptionBase & {
@ -38,6 +39,14 @@ export type THumanitecConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Humanitec;
};
export type TPostgresConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Postgres;
};
export type TMsSqlConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.MsSql;
};
export type TAppConnectionOption =
| TAwsConnectionOption
| TGitHubConnectionOption
@ -45,7 +54,9 @@ export type TAppConnectionOption =
| TAzureAppConfigurationConnectionOption
| TAzureKeyVaultConnectionOption
| TDatabricksConnectionOption
| THumanitecConnectionOption;
| THumanitecConnectionOption
| TPostgresConnectionOption
| TMsSqlConnectionOption;
export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption;
@ -55,4 +66,6 @@ export type TAppConnectionOptionMap = {
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnectionOption;
[AppConnection.Databricks]: TDatabricksConnectionOption;
[AppConnection.Humanitec]: THumanitecConnectionOption;
[AppConnection.Postgres]: TPostgresConnectionOption;
[AppConnection.MsSql]: TMsSqlConnectionOption;
};

View File

@ -2,11 +2,11 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum HumanitecConnectionMethod {
API_TOKEN = "api-token"
ApiToken = "api-token"
}
export type THumanitecConnection = TRootAppConnection & { app: AppConnection.Humanitec } & {
method: HumanitecConnectionMethod.API_TOKEN;
method: HumanitecConnectionMethod.ApiToken;
credentials: {
apiToken: string;
};

View File

@ -1,13 +1,14 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TAppConnectionOption } from "@app/hooks/api/appConnections/types/app-options";
import { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection";
import { TDatabricksConnection } from "@app/hooks/api/appConnections/types/databricks-connection";
import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection";
import { THumanitecConnection } from "@app/hooks/api/appConnections/types/humanitec-connection";
import { AppConnection } from "../enums";
import { TAppConnectionOption } from "./app-options";
import { TAwsConnection } from "./aws-connection";
import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection";
import { TAzureKeyVaultConnection } from "./azure-key-vault-connection";
import { TDatabricksConnection } from "./databricks-connection";
import { TGcpConnection } from "./gcp-connection";
import { TGitHubConnection } from "./github-connection";
import { THumanitecConnection } from "./humanitec-connection";
import { TMsSqlConnection } from "./mssql-connection";
import { TPostgresConnection } from "./postgres-connection";
export * from "./aws-connection";
export * from "./azure-app-configuration-connection";
@ -15,6 +16,7 @@ export * from "./azure-key-vault-connection";
export * from "./gcp-connection";
export * from "./github-connection";
export * from "./humanitec-connection";
export * from "./postgres-connection";
export type TAppConnection =
| TAwsConnection
@ -23,7 +25,9 @@ export type TAppConnection =
| TAzureKeyVaultConnection
| TAzureAppConfigurationConnection
| TDatabricksConnection
| THumanitecConnection;
| THumanitecConnection
| TPostgresConnection
| TMsSqlConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
@ -35,11 +39,11 @@ export type TAvailableAppConnectionsResponse = { appConnections: TAvailableAppCo
export type TCreateAppConnectionDTO = Pick<
TAppConnection,
"name" | "credentials" | "method" | "app" | "description"
"name" | "credentials" | "method" | "app" | "description" | "isPlatformManaged"
>;
export type TUpdateAppConnectionDTO = Partial<
Pick<TAppConnection, "name" | "credentials" | "description">
Pick<TAppConnection, "name" | "credentials" | "description" | "isPlatformManaged">
> & {
connectionId: string;
app: AppConnection;
@ -58,4 +62,6 @@ export type TAppConnectionMap = {
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
[AppConnection.Databricks]: TDatabricksConnection;
[AppConnection.Humanitec]: THumanitecConnection;
[AppConnection.Postgres]: TPostgresConnection;
[AppConnection.MsSql]: TMsSqlConnection;
};

View File

@ -0,0 +1,13 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
import { TBaseSqlConnectionCredentials } from "./shared";
export enum MsSqlConnectionMethod {
UsernameAndPassword = "username-and-password"
}
export type TMsSqlConnection = TRootAppConnection & { app: AppConnection.MsSql } & {
method: MsSqlConnectionMethod.UsernameAndPassword;
credentials: TBaseSqlConnectionCredentials;
};

View File

@ -0,0 +1,13 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
import { TBaseSqlConnectionCredentials } from "./shared";
export enum PostgresConnectionMethod {
UsernameAndPassword = "username-and-password"
}
export type TPostgresConnection = TRootAppConnection & { app: AppConnection.Postgres } & {
method: PostgresConnectionMethod.UsernameAndPassword;
credentials: TBaseSqlConnectionCredentials;
};

View File

@ -6,4 +6,5 @@ export type TRootAppConnection = {
orgId: string;
createdAt: string;
updatedAt: string;
isPlatformManaged?: boolean;
};

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