mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-31 22:09:57 +00:00
Compare commits
7 Commits
0xArshdeep
...
secret-rot
Author | SHA1 | Date | |
---|---|---|---|
bcf86aea90 | |||
06024065cd | |||
2d207147a6 | |||
4c04d0f871 | |||
484a9b28ef | |||
94f79ade4a | |||
5a63a6dbe4 |
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -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
|
||||
|
10
backend/src/@types/knex.d.ts
vendored
10
backend/src/@types/knex.d.ts
vendored
@ -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
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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>;
|
||||
|
@ -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";
|
||||
|
35
backend/src/db/schemas/secret-rotations-v2.ts
Normal file
35
backend/src/db/schemas/secret-rotations-v2.ts
Normal 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>>;
|
@ -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>;
|
||||
|
@ -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" }
|
||||
);
|
||||
};
|
||||
|
12
backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
Normal file
12
backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
Normal 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
|
||||
};
|
@ -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
|
||||
});
|
@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
@ -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 };
|
||||
}
|
||||
});
|
||||
};
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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";
|
@ -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
|
||||
};
|
@ -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)
|
||||
});
|
@ -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;
|
||||
};
|
@ -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 };
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export enum SecretRotation {
|
||||
PostgresCredentials = "postgres-credentials",
|
||||
MsSqlCredentials = "mssql-login-credentials"
|
||||
}
|
@ -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)}...`;
|
||||
// };
|
@ -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
|
||||
};
|
@ -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
|
||||
// };
|
||||
// };
|
@ -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)
|
||||
});
|
@ -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
|
||||
};
|
||||
};
|
@ -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;
|
||||
};
|
@ -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."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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[] = [];
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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
|
||||
});
|
||||
};
|
@ -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
|
||||
});
|
||||
};
|
@ -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 {
|
||||
|
@ -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}`);
|
||||
|
@ -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"
|
||||
};
|
||||
|
@ -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.`)
|
||||
});
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -1,3 +1,3 @@
|
||||
export enum HumanitecConnectionMethod {
|
||||
API_TOKEN = "api-token"
|
||||
ApiToken = "api-token"
|
||||
}
|
||||
|
@ -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]
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
4
backend/src/services/app-connection/mssql/index.ts
Normal file
4
backend/src/services/app-connection/mssql/index.ts
Normal 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";
|
@ -0,0 +1,3 @@
|
||||
export enum MsSqlConnectionMethod {
|
||||
UsernameAndPassword = "username-and-password"
|
||||
}
|
@ -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" });
|
||||
}
|
||||
};
|
@ -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)
|
||||
});
|
@ -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;
|
||||
};
|
4
backend/src/services/app-connection/postgres/index.ts
Normal file
4
backend/src/services/app-connection/postgres/index.ts
Normal 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";
|
@ -0,0 +1,3 @@
|
||||
export enum PostgresConnectionMethod {
|
||||
UsernameAndPassword = "username-and-password"
|
||||
}
|
@ -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" });
|
||||
}
|
||||
};
|
@ -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)
|
||||
});
|
@ -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;
|
||||
};
|
2
backend/src/services/app-connection/shared/sql/index.ts
Normal file
2
backend/src/services/app-connection/shared/sql/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./sql-connection-fns";
|
||||
export * from "./sql-connection-schemas";
|
@ -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;
|
||||
};
|
@ -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()
|
||||
});
|
@ -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>;
|
||||
};
|
@ -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!)
|
||||
);
|
||||
|
||||
|
@ -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)
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/mssql/available"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/mssql"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/mssql/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/mssql/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/mssql/connection-name/{connectionName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/mssql"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/mssql/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Available"
|
||||
openapi: "GET /api/v1/app-connections/postgres/available"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/app-connections/postgres"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v1/app-connections/postgres/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v1/app-connections/postgres/{connectionId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v1/app-connections/postgres/connection-name/{connectionName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v1/app-connections/postgres"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/app-connections/postgres/{connectionId}"
|
||||
---
|
4
docs/api-reference/endpoints/secret-rotations/list.mdx
Normal file
4
docs/api-reference/endpoints/secret-rotations/list.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v2/secret-rotations"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Options"
|
||||
openapi: "GET /api/v2/secret-rotations/options"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Create"
|
||||
openapi: "POST /api/v2/secret-rotations/postgres-credentials"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Delete"
|
||||
openapi: "DELETE /api/v2/secret-rotations/postgres-credentials/{rotationId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by ID"
|
||||
openapi: "GET /api/v2/secret-rotations/postgres-credentials/{rotationId}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get by Name"
|
||||
openapi: "GET /api/v2/secret-rotations/postgres-credentials/rotation-name/{rotationName}"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Get Credentials by ID"
|
||||
openapi: "GET /api/v2/secret-rotations/postgres-credentials/{rotationId}/credentials"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "List"
|
||||
openapi: "GET /api/v2/secret-rotations/postgres-credentials"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Rotate"
|
||||
openapi: "POST /api/v2/secret-rotations/postgres-credentials/{rotationId}/rotate"
|
||||
---
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v2/secret-rotations/postgres-credentials/{rotationId}"
|
||||
---
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
BIN
frontend/public/images/integrations/MsSql.png
Normal file
BIN
frontend/public/images/integrations/MsSql.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
frontend/public/images/integrations/Postgres.png
Normal file
BIN
frontend/public/images/integrations/Postgres.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
@ -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}`);
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
@ -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;
|
||||
};
|
@ -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
Reference in New Issue
Block a user