mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
6 Commits
daniel/upd
...
secret-syn
Author | SHA1 | Date | |
---|---|---|---|
41484239c6 | |||
06c1471f3f | |||
2a987c61eb | |||
e26a67d545 | |||
487a679aa9 | |||
b57bdd869c |
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -80,6 +80,7 @@ import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-
|
|||||||
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
||||||
import { TSecretReplicationServiceFactory } from "@app/services/secret-replication/secret-replication-service";
|
import { TSecretReplicationServiceFactory } from "@app/services/secret-replication/secret-replication-service";
|
||||||
import { TSecretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
|
import { TSecretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
|
||||||
|
import { TSecretSyncServiceFactory } from "@app/services/secret-sync/secret-sync-service";
|
||||||
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
||||||
import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service";
|
import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service";
|
||||||
import { TSlackServiceFactory } from "@app/services/slack/slack-service";
|
import { TSlackServiceFactory } from "@app/services/slack/slack-service";
|
||||||
@ -210,6 +211,7 @@ declare module "fastify" {
|
|||||||
projectTemplate: TProjectTemplateServiceFactory;
|
projectTemplate: TProjectTemplateServiceFactory;
|
||||||
totp: TTotpServiceFactory;
|
totp: TTotpServiceFactory;
|
||||||
appConnection: TAppConnectionServiceFactory;
|
appConnection: TAppConnectionServiceFactory;
|
||||||
|
secretSync: TSecretSyncServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
2
backend/src/@types/knex.d.ts
vendored
2
backend/src/@types/knex.d.ts
vendored
@ -369,6 +369,7 @@ import {
|
|||||||
TExternalGroupOrgRoleMappingsInsert,
|
TExternalGroupOrgRoleMappingsInsert,
|
||||||
TExternalGroupOrgRoleMappingsUpdate
|
TExternalGroupOrgRoleMappingsUpdate
|
||||||
} from "@app/db/schemas/external-group-org-role-mappings";
|
} from "@app/db/schemas/external-group-org-role-mappings";
|
||||||
|
import { TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate } from "@app/db/schemas/secret-syncs";
|
||||||
import {
|
import {
|
||||||
TSecretV2TagJunction,
|
TSecretV2TagJunction,
|
||||||
TSecretV2TagJunctionInsert,
|
TSecretV2TagJunctionInsert,
|
||||||
@ -892,5 +893,6 @@ declare module "knex/types/tables" {
|
|||||||
TAppConnectionsInsert,
|
TAppConnectionsInsert,
|
||||||
TAppConnectionsUpdate
|
TAppConnectionsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.SecretSync]: KnexOriginal.CompositeTableType<TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
46
backend/src/db/migrations/20241219210911_secret-sync.ts
Normal file
46
backend/src/db/migrations/20241219210911_secret-sync.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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.SecretSync))) {
|
||||||
|
await knex.schema.createTable(TableName.SecretSync, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("name", 32).notNullable();
|
||||||
|
t.string("description");
|
||||||
|
t.string("destination").notNullable();
|
||||||
|
t.boolean("isEnabled").notNullable().defaultTo(true);
|
||||||
|
t.integer("version").defaultTo(1).notNullable();
|
||||||
|
t.jsonb("destinationConfig").notNullable();
|
||||||
|
t.jsonb("syncOptions").notNullable();
|
||||||
|
t.uuid("folderId").notNullable();
|
||||||
|
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
|
||||||
|
t.uuid("connectionId").notNullable();
|
||||||
|
t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
// sync
|
||||||
|
t.string("syncStatus");
|
||||||
|
t.string("lastSyncJobId");
|
||||||
|
t.string("lastSyncMessage");
|
||||||
|
t.datetime("lastSyncedAt");
|
||||||
|
// import
|
||||||
|
t.string("importStatus");
|
||||||
|
t.string("lastImportJobId");
|
||||||
|
t.string("lastImportMessage");
|
||||||
|
t.datetime("lastImportedAt");
|
||||||
|
// erase
|
||||||
|
t.string("eraseStatus");
|
||||||
|
t.string("lastEraseJobId");
|
||||||
|
t.string("lastEraseMessage");
|
||||||
|
t.datetime("lastErasedAt");
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SecretSync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SecretSync);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SecretSync);
|
||||||
|
}
|
@ -130,7 +130,8 @@ export enum TableName {
|
|||||||
WorkflowIntegrations = "workflow_integrations",
|
WorkflowIntegrations = "workflow_integrations",
|
||||||
SlackIntegrations = "slack_integrations",
|
SlackIntegrations = "slack_integrations",
|
||||||
ProjectSlackConfigs = "project_slack_configs",
|
ProjectSlackConfigs = "project_slack_configs",
|
||||||
AppConnection = "app_connections"
|
AppConnection = "app_connections",
|
||||||
|
SecretSync = "secret_syncs"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||||
|
39
backend/src/db/schemas/secret-syncs.ts
Normal file
39
backend/src/db/schemas/secret-syncs.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SecretSyncsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
|
destination: z.string(),
|
||||||
|
isEnabled: z.boolean().default(true),
|
||||||
|
version: z.number().default(1),
|
||||||
|
destinationConfig: z.unknown(),
|
||||||
|
syncOptions: z.unknown(),
|
||||||
|
folderId: z.string().uuid(),
|
||||||
|
connectionId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
syncStatus: z.string().nullable().optional(),
|
||||||
|
lastSyncJobId: z.string().nullable().optional(),
|
||||||
|
lastSyncMessage: z.string().nullable().optional(),
|
||||||
|
lastSyncedAt: z.date().nullable().optional(),
|
||||||
|
importStatus: z.string().nullable().optional(),
|
||||||
|
lastImportJobId: z.string().nullable().optional(),
|
||||||
|
lastImportMessage: z.string().nullable().optional(),
|
||||||
|
lastImportedAt: z.date().nullable().optional(),
|
||||||
|
eraseStatus: z.string().nullable().optional(),
|
||||||
|
lastEraseJobId: z.string().nullable().optional(),
|
||||||
|
lastEraseMessage: z.string().nullable().optional(),
|
||||||
|
lastErasedAt: z.date().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSecretSyncs = z.infer<typeof SecretSyncsSchema>;
|
||||||
|
export type TSecretSyncsInsert = Omit<z.input<typeof SecretSyncsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSecretSyncsUpdate = Partial<Omit<z.input<typeof SecretSyncsSchema>, TImmutableDBKeys>>;
|
@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||||
|
import { OrgPermissionSchema } from "@app/ee/services/permission/org-permission";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
@ -24,7 +25,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
),
|
),
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
description: z.string().trim().nullish(),
|
description: z.string().trim().nullish(),
|
||||||
permissions: z.any().array()
|
permissions: OrgPermissionSchema.array()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -96,7 +97,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.optional(),
|
.optional(),
|
||||||
name: z.string().trim().optional(),
|
name: z.string().trim().optional(),
|
||||||
description: z.string().trim().nullish(),
|
description: z.string().trim().nullish(),
|
||||||
permissions: z.any().array().optional()
|
permissions: OrgPermissionSchema.array().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
@ -79,7 +79,8 @@ export const auditLogServiceFactory = ({
|
|||||||
}
|
}
|
||||||
// add all cases in which project id or org id cannot be added
|
// add all cases in which project id or org id cannot be added
|
||||||
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
|
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
|
||||||
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
|
if (!data.projectId && !data.orgId)
|
||||||
|
throw new BadRequestError({ message: "Must specify either project id or org id" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return auditLogQueue.pushToLog(data);
|
return auditLogQueue.pushToLog(data);
|
||||||
|
@ -13,6 +13,13 @@ import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
|||||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||||
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import {
|
||||||
|
TCreateSecretSyncDTO,
|
||||||
|
TDeleteSecretSyncDTO,
|
||||||
|
TSecretSyncRaw,
|
||||||
|
TUpdateSecretSyncDTO
|
||||||
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
export type TListProjectAuditLogDTO = {
|
export type TListProjectAuditLogDTO = {
|
||||||
filter: {
|
filter: {
|
||||||
@ -226,10 +233,19 @@ export enum EventType {
|
|||||||
DELETE_PROJECT_TEMPLATE = "delete-project-template",
|
DELETE_PROJECT_TEMPLATE = "delete-project-template",
|
||||||
APPLY_PROJECT_TEMPLATE = "apply-project-template",
|
APPLY_PROJECT_TEMPLATE = "apply-project-template",
|
||||||
GET_APP_CONNECTIONS = "get-app-connections",
|
GET_APP_CONNECTIONS = "get-app-connections",
|
||||||
|
GET_AVAILABLE_APP_CONNECTIONS_DETAILS = "get-available-app-connections-details",
|
||||||
GET_APP_CONNECTION = "get-app-connection",
|
GET_APP_CONNECTION = "get-app-connection",
|
||||||
CREATE_APP_CONNECTION = "create-app-connection",
|
CREATE_APP_CONNECTION = "create-app-connection",
|
||||||
UPDATE_APP_CONNECTION = "update-app-connection",
|
UPDATE_APP_CONNECTION = "update-app-connection",
|
||||||
DELETE_APP_CONNECTION = "delete-app-connection"
|
DELETE_APP_CONNECTION = "delete-app-connection",
|
||||||
|
GET_SECRET_SYNCS = "get-secret-syncs",
|
||||||
|
GET_SECRET_SYNC = "get-secret-sync",
|
||||||
|
CREATE_SECRET_SYNC = "create-secret-sync",
|
||||||
|
UPDATE_SECRET_SYNC = "update-secret-sync",
|
||||||
|
DELETE_SECRET_SYNC = "delete-secret-sync",
|
||||||
|
SYNC_SECRET_SYNC = "sync-secret-sync",
|
||||||
|
IMPORT_SECRET_SYNC = "import-secret-sync",
|
||||||
|
ERASE_SECRET_SYNC = "erase-secret-sync"
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActorMetadata {
|
interface UserActorMetadata {
|
||||||
@ -1883,6 +1899,15 @@ interface GetAppConnectionsEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetAvailableAppConnectionsDetailsEvent {
|
||||||
|
type: EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS;
|
||||||
|
metadata: {
|
||||||
|
app?: AppConnection;
|
||||||
|
count: number;
|
||||||
|
connectionIds: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface GetAppConnectionEvent {
|
interface GetAppConnectionEvent {
|
||||||
type: EventType.GET_APP_CONNECTION;
|
type: EventType.GET_APP_CONNECTION;
|
||||||
metadata: {
|
metadata: {
|
||||||
@ -1907,6 +1932,77 @@ interface DeleteAppConnectionEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetSecretSyncsEvent {
|
||||||
|
type: EventType.GET_SECRET_SYNCS;
|
||||||
|
metadata: {
|
||||||
|
destination?: SecretSync;
|
||||||
|
count: number;
|
||||||
|
syncIds: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSecretSyncEvent {
|
||||||
|
type: EventType.GET_SECRET_SYNC;
|
||||||
|
metadata: {
|
||||||
|
destination: SecretSync;
|
||||||
|
syncId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateSecretSyncEvent {
|
||||||
|
type: EventType.CREATE_SECRET_SYNC;
|
||||||
|
metadata: TCreateSecretSyncDTO & { syncId: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateSecretSyncEvent {
|
||||||
|
type: EventType.UPDATE_SECRET_SYNC;
|
||||||
|
metadata: TUpdateSecretSyncDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteSecretSyncEvent {
|
||||||
|
type: EventType.DELETE_SECRET_SYNC;
|
||||||
|
metadata: TDeleteSecretSyncDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SyncSecretSyncEvent {
|
||||||
|
type: EventType.SYNC_SECRET_SYNC;
|
||||||
|
metadata: Pick<
|
||||||
|
TSecretSyncRaw,
|
||||||
|
"syncOptions" | "destinationConfig" | "destination" | "syncStatus" | "environment" | "connectionId" | "folderId"
|
||||||
|
> & {
|
||||||
|
syncId: string;
|
||||||
|
syncMessage: string | null;
|
||||||
|
jobId: string;
|
||||||
|
jobRanAt: Date;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportSecretSyncEvent {
|
||||||
|
type: EventType.IMPORT_SECRET_SYNC;
|
||||||
|
metadata: Pick<
|
||||||
|
TSecretSyncRaw,
|
||||||
|
"syncOptions" | "destinationConfig" | "destination" | "importStatus" | "environment" | "connectionId" | "folderId"
|
||||||
|
> & {
|
||||||
|
syncId: string;
|
||||||
|
importMessage: string | null;
|
||||||
|
jobId: string;
|
||||||
|
jobRanAt: Date;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EraseSecretSyncEvent {
|
||||||
|
type: EventType.ERASE_SECRET_SYNC;
|
||||||
|
metadata: Pick<
|
||||||
|
TSecretSyncRaw,
|
||||||
|
"syncOptions" | "destinationConfig" | "destination" | "eraseStatus" | "environment" | "connectionId" | "folderId"
|
||||||
|
> & {
|
||||||
|
syncId: string;
|
||||||
|
eraseMessage: string | null;
|
||||||
|
jobId: string;
|
||||||
|
jobRanAt: Date;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type Event =
|
export type Event =
|
||||||
| GetSecretsEvent
|
| GetSecretsEvent
|
||||||
| GetSecretEvent
|
| GetSecretEvent
|
||||||
@ -2080,7 +2176,16 @@ export type Event =
|
|||||||
| DeleteProjectTemplateEvent
|
| DeleteProjectTemplateEvent
|
||||||
| ApplyProjectTemplateEvent
|
| ApplyProjectTemplateEvent
|
||||||
| GetAppConnectionsEvent
|
| GetAppConnectionsEvent
|
||||||
|
| GetAvailableAppConnectionsDetailsEvent
|
||||||
| GetAppConnectionEvent
|
| GetAppConnectionEvent
|
||||||
| CreateAppConnectionEvent
|
| CreateAppConnectionEvent
|
||||||
| UpdateAppConnectionEvent
|
| UpdateAppConnectionEvent
|
||||||
| DeleteAppConnectionEvent;
|
| DeleteAppConnectionEvent
|
||||||
|
| GetSecretSyncsEvent
|
||||||
|
| GetSecretSyncEvent
|
||||||
|
| CreateSecretSyncEvent
|
||||||
|
| UpdateSecretSyncEvent
|
||||||
|
| DeleteSecretSyncEvent
|
||||||
|
| SyncSecretSyncEvent
|
||||||
|
| ImportSecretSyncEvent
|
||||||
|
| EraseSecretSyncEvent;
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability";
|
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CASL_ACTION_SCHEMA_ENUM,
|
||||||
|
CASL_ACTION_SCHEMA_NATIVE_ENUM
|
||||||
|
} from "@app/ee/services/permission/permission-schemas";
|
||||||
|
import { PermissionConditionSchema } from "@app/ee/services/permission/permission-types";
|
||||||
|
import { PermissionConditionOperators } from "@app/lib/casl";
|
||||||
|
|
||||||
export enum OrgPermissionActions {
|
export enum OrgPermissionActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
@ -7,6 +15,14 @@ export enum OrgPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrgPermissionAppConnectionActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete",
|
||||||
|
Connect = "connect"
|
||||||
|
}
|
||||||
|
|
||||||
export enum OrgPermissionAdminConsoleAction {
|
export enum OrgPermissionAdminConsoleAction {
|
||||||
AccessAllProjects = "access-all-projects"
|
AccessAllProjects = "access-all-projects"
|
||||||
}
|
}
|
||||||
@ -31,6 +47,10 @@ export enum OrgPermissionSubjects {
|
|||||||
AppConnections = "app-connections"
|
AppConnections = "app-connections"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AppConnectionSubjectFields = {
|
||||||
|
connectionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
|
| [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Role]
|
| [OrgPermissionActions, OrgPermissionSubjects.Role]
|
||||||
@ -47,9 +67,109 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.AppConnections]
|
| [
|
||||||
|
OrgPermissionAppConnectionActions,
|
||||||
|
(
|
||||||
|
| OrgPermissionSubjects.AppConnections
|
||||||
|
| (ForcedSubject<OrgPermissionSubjects.AppConnections> & AppConnectionSubjectFields)
|
||||||
|
)
|
||||||
|
]
|
||||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||||
|
|
||||||
|
const AppConnectionConditionSchema = z
|
||||||
|
.object({
|
||||||
|
connectionId: z.union([
|
||||||
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||||
|
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||||
|
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
|
export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Workspace).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_ENUM([OrgPermissionActions.Create]).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Role).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Member).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Settings).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.IncidentAccount).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Sso).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Scim).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Ldap).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Groups).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.SecretScanning).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Billing).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Identity).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.Kms).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.AuditLogs).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.ProjectTemplates).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.AppConnections).describe("The entity this permission pertains to."),
|
||||||
|
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAppConnectionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
),
|
||||||
|
conditions: AppConnectionConditionSchema.describe(
|
||||||
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
|
).optional()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(OrgPermissionSubjects.AdminConsole).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAdminConsoleAction).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, rules } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
// ws permissions
|
// ws permissions
|
||||||
@ -125,10 +245,16 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
can(
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
|
[
|
||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
|
OrgPermissionAppConnectionActions.Create,
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections);
|
OrgPermissionAppConnectionActions.Edit,
|
||||||
|
OrgPermissionAppConnectionActions.Delete,
|
||||||
|
OrgPermissionAppConnectionActions.Read,
|
||||||
|
OrgPermissionAppConnectionActions.Connect
|
||||||
|
],
|
||||||
|
OrgPermissionSubjects.AppConnections
|
||||||
|
);
|
||||||
|
|
||||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||||
|
|
||||||
@ -160,7 +286,7 @@ const buildMemberPermission = () => {
|
|||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
9
backend/src/ee/services/permission/permission-schemas.ts
Normal file
9
backend/src/ee/services/permission/permission-schemas.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTION) =>
|
||||||
|
z
|
||||||
|
.union([z.nativeEnum(actions), z.nativeEnum(actions).array().min(1)])
|
||||||
|
.transform((el) => (typeof el === "string" ? [el] : el));
|
||||||
|
|
||||||
|
export const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
|
||||||
|
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));
|
@ -1,6 +1,10 @@
|
|||||||
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CASL_ACTION_SCHEMA_ENUM,
|
||||||
|
CASL_ACTION_SCHEMA_NATIVE_ENUM
|
||||||
|
} from "@app/ee/services/permission/permission-schemas";
|
||||||
import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl";
|
import { conditionsMatcher, PermissionConditionOperators } from "@app/lib/casl";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
||||||
|
|
||||||
@ -60,7 +64,8 @@ export enum ProjectPermissionSub {
|
|||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
Cmek = "cmek"
|
Cmek = "cmek",
|
||||||
|
SecretSyncs = "secret-syncs"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SecretSubjectFields = {
|
export type SecretSubjectFields = {
|
||||||
@ -140,6 +145,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretSyncs]
|
||||||
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||||
@ -147,14 +153,6 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||||
|
|
||||||
const CASL_ACTION_SCHEMA_NATIVE_ENUM = <ACTION extends z.EnumLike>(actions: ACTION) =>
|
|
||||||
z
|
|
||||||
.union([z.nativeEnum(actions), z.nativeEnum(actions).array().min(1)])
|
|
||||||
.transform((el) => (typeof el === "string" ? [el] : el));
|
|
||||||
|
|
||||||
const CASL_ACTION_SCHEMA_ENUM = <ACTION extends z.EnumValues>(actions: ACTION) =>
|
|
||||||
z.union([z.enum(actions), z.enum(actions).array().min(1)]).transform((el) => (typeof el === "string" ? [el] : el));
|
|
||||||
|
|
||||||
// akhilmhdh: don't modify this for v2
|
// akhilmhdh: don't modify this for v2
|
||||||
// if you want to update create a new schema
|
// if you want to update create a new schema
|
||||||
const SecretConditionV1Schema = z
|
const SecretConditionV1Schema = z
|
||||||
@ -392,10 +390,15 @@ const GeneralPermissionSchema = [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.Cmek).describe("The entity this permission pertains to."),
|
||||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCmekActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -511,7 +514,8 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionSub.PkiCollections,
|
ProjectPermissionSub.PkiCollections,
|
||||||
ProjectPermissionSub.SshCertificateAuthorities,
|
ProjectPermissionSub.SshCertificateAuthorities,
|
||||||
ProjectPermissionSub.SshCertificates,
|
ProjectPermissionSub.SshCertificates,
|
||||||
ProjectPermissionSub.SshCertificateTemplates
|
ProjectPermissionSub.SshCertificateTemplates,
|
||||||
|
ProjectPermissionSub.SecretSyncs
|
||||||
].forEach((el) => {
|
].forEach((el) => {
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
@ -713,6 +717,16 @@ const buildMemberPermissionRules = () => {
|
|||||||
ProjectPermissionSub.Cmek
|
ProjectPermissionSub.Cmek
|
||||||
);
|
);
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionActions.Delete
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.SecretSyncs
|
||||||
|
);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -746,6 +760,7 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||||
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||||
|
|
||||||
return rules;
|
return rules;
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,8 @@ export const KeyStorePrefixes = {
|
|||||||
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
|
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||||
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||||
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
|
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||||
|
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
|
||||||
|
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
|
||||||
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
||||||
`identity-access-token-status:${identityAccessTokenId}`,
|
`identity-access-token-status:${identityAccessTokenId}`,
|
||||||
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`
|
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`
|
||||||
@ -30,6 +32,7 @@ export const KeyStorePrefixes = {
|
|||||||
|
|
||||||
export const KeyStoreTtls = {
|
export const KeyStoreTtls = {
|
||||||
SetSyncSecretIntegrationLastRunTimestampInSeconds: 60,
|
SetSyncSecretIntegrationLastRunTimestampInSeconds: 60,
|
||||||
|
SetSecretSyncLastRunTimestampInSeconds: 60,
|
||||||
AccessTokenStatusUpdateInSeconds: 120
|
AccessTokenStatusUpdateInSeconds: 120
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
||||||
|
|
||||||
export const GROUPS = {
|
export const GROUPS = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
@ -1636,6 +1638,66 @@ export const AppConnections = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
DELETE: (app: AppConnection) => ({
|
DELETE: (app: AppConnection) => ({
|
||||||
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} connection to be deleted.`
|
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} Connection to be deleted.`
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SecretSyncs = {
|
||||||
|
LIST: (destination?: SecretSync) => ({
|
||||||
|
projectId: `The ID of the project to list ${destination ? SECRET_SYNC_NAME_MAP[destination] : "Secret"} Syncs from.`
|
||||||
|
}),
|
||||||
|
GET_BY_ID: (destination: SecretSync) => ({
|
||||||
|
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to retrieve.`
|
||||||
|
}),
|
||||||
|
GET_BY_NAME: (destination: SecretSync) => ({
|
||||||
|
syncName: `The name of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to retrieve.`,
|
||||||
|
projectId: `The ID of the project the ${SECRET_SYNC_NAME_MAP[destination]} Sync is associated with.`
|
||||||
|
}),
|
||||||
|
CREATE: (destination: SecretSync) => {
|
||||||
|
const destinationName = SECRET_SYNC_NAME_MAP[destination];
|
||||||
|
return {
|
||||||
|
name: `The name of the ${destinationName} Sync to create. Must be slug-friendly.`,
|
||||||
|
description: `An optional description for the ${destinationName} Sync.`,
|
||||||
|
folderId: `The ID of the project folder to sync secrets from.`,
|
||||||
|
connectionId: `The ID of the ${
|
||||||
|
APP_CONNECTION_NAME_MAP[SECRET_SYNC_CONNECTION_MAP[destination]]
|
||||||
|
} Connection to use for syncing.`,
|
||||||
|
isEnabled: `Whether secrets should be synced automatically or not.`,
|
||||||
|
syncOptions: "Optional parameters to modify how secrets are synced."
|
||||||
|
};
|
||||||
|
},
|
||||||
|
UPDATE: (destination: SecretSync) => {
|
||||||
|
const destinationName = SECRET_SYNC_NAME_MAP[destination];
|
||||||
|
return {
|
||||||
|
syncId: `The ID of the ${destinationName} Sync to be updated.`,
|
||||||
|
name: `The updated name of the ${destinationName} Sync. Must be slug-friendly.`,
|
||||||
|
folderId: `The updated project folder ID to sync secrets from.`,
|
||||||
|
description: `The updated description of the ${destinationName} Sync.`,
|
||||||
|
isEnabled: `Whether secrets should be synced automatically or not.`,
|
||||||
|
syncOptions: "Optional parameters to modify how secrets are synced."
|
||||||
|
};
|
||||||
|
},
|
||||||
|
DELETE: (destination: SecretSync) => ({
|
||||||
|
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to be deleted.`
|
||||||
|
}),
|
||||||
|
SYNC: (destination: SecretSync) => ({
|
||||||
|
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to trigger a sync for.`
|
||||||
|
}),
|
||||||
|
IMPORT: (destination: SecretSync) => ({
|
||||||
|
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to trigger an import for.`,
|
||||||
|
shouldOverwrite: `Specify whether newly imported secrets should override existing secrets with matching names in Infisical.`
|
||||||
|
}),
|
||||||
|
ERASE: (destination: SecretSync) => ({
|
||||||
|
syncId: `The ID of the ${SECRET_SYNC_NAME_MAP[destination]} Sync to trigger an erase for.`
|
||||||
|
}),
|
||||||
|
SYNC_OPTIONS: {
|
||||||
|
PREPEND_PREFIX: "Optionally prepend a prefix to your secrets' keys when syncing.",
|
||||||
|
APPEND_SUFFIX: "Optionally append a suffix to your secrets' keys when syncing."
|
||||||
|
},
|
||||||
|
DESTINATION_CONFIG: {
|
||||||
|
AWS_PARAMETER_STORE: {
|
||||||
|
REGION: "The AWS region to sync secrets to.",
|
||||||
|
PATH: "The Parameter Store path to sync secrets to."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -16,3 +16,7 @@ export const prefixWithSlash = (str: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const startsWithVowel = (str: string) => /^[aeiou]/i.test(str);
|
export const startsWithVowel = (str: string) => /^[aeiou]/i.test(str);
|
||||||
|
|
||||||
|
export const wrapWithSlashes = (str: string) => {
|
||||||
|
return `${str.startsWith("/") ? "" : "/"}${str}${str.endsWith("/") ? "" : `/`}`;
|
||||||
|
};
|
||||||
|
@ -15,6 +15,12 @@ import {
|
|||||||
TIntegrationSyncPayload,
|
TIntegrationSyncPayload,
|
||||||
TSyncSecretsDTO
|
TSyncSecretsDTO
|
||||||
} from "@app/services/secret/secret-types";
|
} from "@app/services/secret/secret-types";
|
||||||
|
import {
|
||||||
|
TQueueSecretSyncByIdDTO,
|
||||||
|
TQueueSecretSyncEraseByIdDTO,
|
||||||
|
TQueueSecretSyncImportByIdDTO,
|
||||||
|
TQueueSendSecretSyncActionFailedNotificationsDTO
|
||||||
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
export enum QueueName {
|
export enum QueueName {
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
@ -36,7 +42,8 @@ export enum QueueName {
|
|||||||
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
|
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
|
||||||
ProjectV3Migration = "project-v3-migration",
|
ProjectV3Migration = "project-v3-migration",
|
||||||
AccessTokenStatusUpdate = "access-token-status-update",
|
AccessTokenStatusUpdate = "access-token-status-update",
|
||||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source"
|
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
||||||
|
AppConnectionSecretSync = "app-connection-secret-sync"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueueJobs {
|
export enum QueueJobs {
|
||||||
@ -61,7 +68,11 @@ export enum QueueJobs {
|
|||||||
ProjectV3Migration = "project-v3-migration",
|
ProjectV3Migration = "project-v3-migration",
|
||||||
IdentityAccessTokenStatusUpdate = "identity-access-token-status-update",
|
IdentityAccessTokenStatusUpdate = "identity-access-token-status-update",
|
||||||
ServiceTokenStatusUpdate = "service-token-status-update",
|
ServiceTokenStatusUpdate = "service-token-status-update",
|
||||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source"
|
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
||||||
|
AppConnectionSecretSync = "app-connection-secret-sync",
|
||||||
|
AppConnectionSecretSyncImport = "app-connection-secret-sync-import",
|
||||||
|
AppConnectionSecretSyncErase = "app-connection-secret-sync-erase",
|
||||||
|
AppConnectionSendSecretSyncActionFailedNotifications = "app-connection-send-secret-sync-action-failed-notifications"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TQueueJobTypes = {
|
export type TQueueJobTypes = {
|
||||||
@ -184,6 +195,23 @@ export type TQueueJobTypes = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
[QueueName.AppConnectionSecretSync]:
|
||||||
|
| {
|
||||||
|
name: QueueJobs.AppConnectionSecretSync;
|
||||||
|
payload: TQueueSecretSyncByIdDTO;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: QueueJobs.AppConnectionSecretSyncImport;
|
||||||
|
payload: TQueueSecretSyncImportByIdDTO;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: QueueJobs.AppConnectionSecretSyncErase;
|
||||||
|
payload: TQueueSecretSyncEraseByIdDTO;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: QueueJobs.AppConnectionSendSecretSyncActionFailedNotifications;
|
||||||
|
payload: TQueueSendSecretSyncActionFailedNotificationsDTO;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||||
|
@ -195,6 +195,9 @@ import { secretImportDALFactory } from "@app/services/secret-import/secret-impor
|
|||||||
import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
||||||
import { secretSharingDALFactory } from "@app/services/secret-sharing/secret-sharing-dal";
|
import { secretSharingDALFactory } from "@app/services/secret-sharing/secret-sharing-dal";
|
||||||
import { secretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
|
import { secretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
|
||||||
|
import { secretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal";
|
||||||
|
import { secretSyncQueueFactory } from "@app/services/secret-sync/secret-sync-queue";
|
||||||
|
import { secretSyncServiceFactory } from "@app/services/secret-sync/secret-sync-service";
|
||||||
import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||||
import { secretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
import { secretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
||||||
import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
|
import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
@ -317,6 +320,7 @@ export const registerRoutes = async (
|
|||||||
const trustedIpDAL = trustedIpDALFactory(db);
|
const trustedIpDAL = trustedIpDALFactory(db);
|
||||||
const telemetryDAL = telemetryDALFactory(db);
|
const telemetryDAL = telemetryDALFactory(db);
|
||||||
const appConnectionDAL = appConnectionDALFactory(db);
|
const appConnectionDAL = appConnectionDALFactory(db);
|
||||||
|
const secretSyncDAL = secretSyncDALFactory(db, folderDAL);
|
||||||
|
|
||||||
// ee db layer ops
|
// ee db layer ops
|
||||||
const permissionDAL = permissionDALFactory(db);
|
const permissionDAL = permissionDALFactory(db);
|
||||||
@ -821,6 +825,28 @@ export const registerRoutes = async (
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const secretSyncQueue = secretSyncQueueFactory({
|
||||||
|
queueService,
|
||||||
|
secretSyncDAL,
|
||||||
|
folderDAL,
|
||||||
|
secretImportDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
kmsService,
|
||||||
|
keyStore,
|
||||||
|
auditLogService,
|
||||||
|
smtpService,
|
||||||
|
projectDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretBlindIndexDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagDAL,
|
||||||
|
secretVersionV2BridgeDAL,
|
||||||
|
secretVersionTagV2BridgeDAL
|
||||||
|
});
|
||||||
|
|
||||||
const secretQueueService = secretQueueFactory({
|
const secretQueueService = secretQueueFactory({
|
||||||
keyStore,
|
keyStore,
|
||||||
queueService,
|
queueService,
|
||||||
@ -854,7 +880,8 @@ export const registerRoutes = async (
|
|||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
orgService
|
orgService,
|
||||||
|
secretSyncQueue
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectService = projectServiceFactory({
|
const projectService = projectServiceFactory({
|
||||||
@ -1362,6 +1389,17 @@ export const registerRoutes = async (
|
|||||||
licenseService
|
licenseService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const secretSyncService = secretSyncServiceFactory({
|
||||||
|
secretSyncDAL,
|
||||||
|
permissionService,
|
||||||
|
appConnectionService,
|
||||||
|
licenseService,
|
||||||
|
folderDAL,
|
||||||
|
secretSyncQueue,
|
||||||
|
projectBotService,
|
||||||
|
keyStore
|
||||||
|
});
|
||||||
|
|
||||||
await superAdminService.initServerCfg();
|
await superAdminService.initServerCfg();
|
||||||
|
|
||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
@ -1459,7 +1497,8 @@ export const registerRoutes = async (
|
|||||||
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
|
externalGroupOrgRoleMapping: externalGroupOrgRoleMappingService,
|
||||||
projectTemplate: projectTemplateService,
|
projectTemplate: projectTemplateService,
|
||||||
totp: totpService,
|
totp: totpService,
|
||||||
appConnection: appConnectionService
|
appConnection: appConnectionService,
|
||||||
|
secretSync: secretSyncService
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronJobs: CronJob[] = [];
|
const cronJobs: CronJob[] = [];
|
||||||
|
@ -15,7 +15,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
app,
|
app,
|
||||||
createSchema,
|
createSchema,
|
||||||
updateSchema,
|
updateSchema,
|
||||||
responseSchema
|
sanitizedResponseSchema
|
||||||
}: {
|
}: {
|
||||||
app: AppConnection;
|
app: AppConnection;
|
||||||
server: FastifyZodProvider;
|
server: FastifyZodProvider;
|
||||||
@ -26,7 +26,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
description?: string | null;
|
description?: string | null;
|
||||||
}>;
|
}>;
|
||||||
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
|
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
|
||||||
responseSchema: z.ZodTypeAny;
|
sanitizedResponseSchema: z.ZodTypeAny;
|
||||||
}) => {
|
}) => {
|
||||||
const appName = APP_CONNECTION_NAME_MAP[app];
|
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
schema: {
|
schema: {
|
||||||
description: `List the ${appName} Connections for the current organization.`,
|
description: `List the ${appName} Connections for the current organization.`,
|
||||||
response: {
|
response: {
|
||||||
200: z.object({ appConnections: responseSchema.array() })
|
200: z.object({ appConnections: sanitizedResponseSchema.array() })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
@ -63,6 +63,44 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/available",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `List the ${appName} Connections the current user has permission to establish connections with.`,
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
appConnections: z.object({ app: z.literal(app), name: z.string(), id: z.string().uuid() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const appConnections = await server.services.appConnection.listAvailableAppConnectionsForUser(
|
||||||
|
app,
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS,
|
||||||
|
metadata: {
|
||||||
|
app,
|
||||||
|
count: appConnections.length,
|
||||||
|
connectionIds: appConnections.map((connection) => connection.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { appConnections };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:connectionId",
|
url: "/:connectionId",
|
||||||
@ -75,7 +113,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
connectionId: z.string().uuid().describe(AppConnections.GET_BY_ID(app).connectionId)
|
connectionId: z.string().uuid().describe(AppConnections.GET_BY_ID(app).connectionId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({ appConnection: responseSchema })
|
200: z.object({ appConnection: sanitizedResponseSchema })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
@ -114,11 +152,12 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
connectionName: z
|
connectionName: z
|
||||||
.string()
|
.string()
|
||||||
.min(0, "Connection name required")
|
.trim()
|
||||||
|
.min(1, "Connection name required")
|
||||||
.describe(AppConnections.GET_BY_NAME(app).connectionName)
|
.describe(AppConnections.GET_BY_NAME(app).connectionName)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({ appConnection: responseSchema })
|
200: z.object({ appConnection: sanitizedResponseSchema })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
@ -158,7 +197,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
} ${appName} Connection for the current organization.`,
|
} ${appName} Connection for the current organization.`,
|
||||||
body: createSchema,
|
body: createSchema,
|
||||||
response: {
|
response: {
|
||||||
200: z.object({ appConnection: responseSchema })
|
200: z.object({ appConnection: sanitizedResponseSchema })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
@ -168,7 +207,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
const appConnection = (await server.services.appConnection.createAppConnection(
|
const appConnection = (await server.services.appConnection.createAppConnection(
|
||||||
{ name, method, app, credentials, description },
|
{ name, method, app, credentials, description },
|
||||||
req.permission
|
req.permission
|
||||||
)) as TAppConnection;
|
)) as T;
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
@ -201,7 +240,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
}),
|
}),
|
||||||
body: updateSchema,
|
body: updateSchema,
|
||||||
response: {
|
response: {
|
||||||
200: z.object({ appConnection: responseSchema })
|
200: z.object({ appConnection: sanitizedResponseSchema })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
@ -244,7 +283,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
connectionId: z.string().uuid().describe(AppConnections.DELETE(app).connectionId)
|
connectionId: z.string().uuid().describe(AppConnections.DELETE(app).connectionId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({ appConnection: responseSchema })
|
200: z.object({ appConnection: sanitizedResponseSchema })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
@ -1,8 +0,0 @@
|
|||||||
import { registerAwsConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/aws-connection-router";
|
|
||||||
import { registerGitHubConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/github-connection-router";
|
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
|
||||||
|
|
||||||
export const APP_CONNECTION_REGISTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> = {
|
|
||||||
[AppConnection.AWS]: registerAwsConnectionRouter,
|
|
||||||
[AppConnection.GitHub]: registerGitHubConnectionRouter
|
|
||||||
};
|
|
@ -11,7 +11,7 @@ export const registerAwsConnectionRouter = async (server: FastifyZodProvider) =>
|
|||||||
registerAppConnectionEndpoints({
|
registerAppConnectionEndpoints({
|
||||||
app: AppConnection.AWS,
|
app: AppConnection.AWS,
|
||||||
server,
|
server,
|
||||||
responseSchema: SanitizedAwsConnectionSchema,
|
sanitizedResponseSchema: SanitizedAwsConnectionSchema,
|
||||||
createSchema: CreateAwsConnectionSchema,
|
createSchema: CreateAwsConnectionSchema,
|
||||||
updateSchema: UpdateAwsConnectionSchema
|
updateSchema: UpdateAwsConnectionSchema
|
||||||
});
|
});
|
@ -11,7 +11,7 @@ export const registerGitHubConnectionRouter = async (server: FastifyZodProvider)
|
|||||||
registerAppConnectionEndpoints({
|
registerAppConnectionEndpoints({
|
||||||
app: AppConnection.GitHub,
|
app: AppConnection.GitHub,
|
||||||
server,
|
server,
|
||||||
responseSchema: SanitizedGitHubConnectionSchema,
|
sanitizedResponseSchema: SanitizedGitHubConnectionSchema,
|
||||||
createSchema: CreateGitHubConnectionSchema,
|
createSchema: CreateGitHubConnectionSchema,
|
||||||
updateSchema: UpdateGitHubConnectionSchema
|
updateSchema: UpdateGitHubConnectionSchema
|
||||||
});
|
});
|
@ -1,2 +1,12 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||||
|
import { registerGitHubConnectionRouter } from "./github-connection-router";
|
||||||
|
|
||||||
export * from "./app-connection-router";
|
export * from "./app-connection-router";
|
||||||
export * from "./apps";
|
|
||||||
|
export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> =
|
||||||
|
{
|
||||||
|
[AppConnection.AWS]: registerAwsConnectionRouter,
|
||||||
|
[AppConnection.GitHub]: registerGitHubConnectionRouter
|
||||||
|
};
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { APP_CONNECTION_REGISTER_MAP, registerAppConnectionRouter } from "@app/server/routes/v1/app-connection-routers";
|
import {
|
||||||
|
APP_CONNECTION_REGISTER_ROUTER_MAP,
|
||||||
|
registerAppConnectionRouter
|
||||||
|
} from "@app/server/routes/v1/app-connection-routers";
|
||||||
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
|
import { registerCmekRouter } from "@app/server/routes/v1/cmek-router";
|
||||||
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
|
import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router";
|
||||||
|
import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/server/routes/v1/secret-sync-routers";
|
||||||
|
|
||||||
import { registerAdminRouter } from "./admin-router";
|
import { registerAdminRouter } from "./admin-router";
|
||||||
import { registerAuthRoutes } from "./auth-router";
|
import { registerAuthRoutes } from "./auth-router";
|
||||||
@ -113,12 +117,28 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
|
await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" });
|
||||||
|
|
||||||
await server.register(
|
await server.register(
|
||||||
async (appConnectionsRouter) => {
|
async (appConnectionRouter) => {
|
||||||
await appConnectionsRouter.register(registerAppConnectionRouter);
|
// register generic app connection endpoints
|
||||||
for await (const [app, router] of Object.entries(APP_CONNECTION_REGISTER_MAP)) {
|
await appConnectionRouter.register(registerAppConnectionRouter);
|
||||||
await appConnectionsRouter.register(router, { prefix: `/${app}` });
|
|
||||||
|
// register service specific endpoints (app-connections/aws, app-connections/github, etc.)
|
||||||
|
for await (const [app, router] of Object.entries(APP_CONNECTION_REGISTER_ROUTER_MAP)) {
|
||||||
|
await appConnectionRouter.register(router, { prefix: `/${app}` });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ prefix: "/app-connections" }
|
{ prefix: "/app-connections" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await server.register(
|
||||||
|
async (secretSyncRouter) => {
|
||||||
|
// register generic secret sync endpoints
|
||||||
|
await secretSyncRouter.register(registerSecretSyncRouter);
|
||||||
|
|
||||||
|
// register service specific secret sync endpoints (secret-syncs/aws-parameter-store, secret-syncs/github, etc.)
|
||||||
|
for await (const [destination, router] of Object.entries(SECRET_SYNC_REGISTER_ROUTER_MAP)) {
|
||||||
|
await secretSyncRouter.register(router, { prefix: `/${destination}` });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ prefix: "/secret-syncs" }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
AwsParameterStoreSyncSchema,
|
||||||
|
CreateAwsParameterStoreSyncSchema,
|
||||||
|
UpdateAwsParameterStoreSyncSchema
|
||||||
|
} from "@app/services/secret-sync/aws-parameter-store";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
|
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||||
|
|
||||||
|
export const registerAwsParameterStoreSyncRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSyncSecretsEndpoints({
|
||||||
|
destination: SecretSync.AWSParameterStore,
|
||||||
|
server,
|
||||||
|
responseSchema: AwsParameterStoreSyncSchema,
|
||||||
|
createSchema: CreateAwsParameterStoreSyncSchema,
|
||||||
|
updateSchema: UpdateAwsParameterStoreSyncSchema
|
||||||
|
});
|
@ -0,0 +1,9 @@
|
|||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
|
import { registerAwsParameterStoreSyncRouter } from "./aws-parameter-store-sync-router";
|
||||||
|
|
||||||
|
export * from "./secret-sync-router";
|
||||||
|
|
||||||
|
export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: FastifyZodProvider) => Promise<void>> = {
|
||||||
|
[SecretSync.AWSParameterStore]: registerAwsParameterStoreSyncRouter
|
||||||
|
};
|
@ -0,0 +1,398 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { SecretSyncs } 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";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
||||||
|
import { TSecretSync, TSecretSyncInput } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TSecretSyncInput>({
|
||||||
|
server,
|
||||||
|
destination,
|
||||||
|
createSchema,
|
||||||
|
updateSchema,
|
||||||
|
responseSchema
|
||||||
|
}: {
|
||||||
|
destination: SecretSync;
|
||||||
|
server: FastifyZodProvider;
|
||||||
|
createSchema: z.ZodType<{
|
||||||
|
name: string;
|
||||||
|
folderId: string;
|
||||||
|
connectionId: string;
|
||||||
|
destinationConfig: I["destinationConfig"];
|
||||||
|
syncOptions?: I["syncOptions"];
|
||||||
|
description?: string | null;
|
||||||
|
}>;
|
||||||
|
updateSchema: z.ZodType<{
|
||||||
|
name?: string;
|
||||||
|
folderId?: string;
|
||||||
|
destinationConfig?: I["destinationConfig"];
|
||||||
|
syncOptions?: I["syncOptions"];
|
||||||
|
description?: string | null;
|
||||||
|
}>;
|
||||||
|
responseSchema: z.ZodTypeAny;
|
||||||
|
}) => {
|
||||||
|
const destinationName = SECRET_SYNC_NAME_MAP[destination];
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `List the ${destinationName} Syncs for the specified project.`,
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.LIST(destination).projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretSyncs: responseSchema.array() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
query: { projectId }
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const secretSyncs = (await server.services.secretSync.listSecretSyncsByProjectId(
|
||||||
|
{ projectId, destination },
|
||||||
|
req.permission
|
||||||
|
)) as T[];
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRET_SYNCS,
|
||||||
|
metadata: {
|
||||||
|
destination,
|
||||||
|
count: secretSyncs.length,
|
||||||
|
syncIds: secretSyncs.map((connection) => connection.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretSyncs };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:syncId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Get the specified ${destinationName} Sync by ID.`,
|
||||||
|
params: z.object({
|
||||||
|
syncId: z.string().uuid().describe(SecretSyncs.GET_BY_ID(destination).syncId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretSync: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { syncId } = req.params;
|
||||||
|
|
||||||
|
const secretSync = (await server.services.secretSync.findSecretSyncById(
|
||||||
|
{ syncId, destination },
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: secretSync.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRET_SYNC,
|
||||||
|
metadata: {
|
||||||
|
syncId,
|
||||||
|
destination
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretSync };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/name/:syncName`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Get the specified ${destinationName} Sync by name and project ID.`,
|
||||||
|
params: z.object({
|
||||||
|
syncName: z.string().trim().min(1, "Sync name required").describe(SecretSyncs.GET_BY_NAME(destination).syncName)
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Project ID required")
|
||||||
|
.describe(SecretSyncs.GET_BY_NAME(destination).projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretSync: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { syncName } = req.params;
|
||||||
|
const { projectId } = req.query;
|
||||||
|
|
||||||
|
const secretSync = (await server.services.secretSync.findSecretSyncByName(
|
||||||
|
{ syncName, projectId, destination },
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRET_SYNC,
|
||||||
|
metadata: {
|
||||||
|
syncId: secretSync.id,
|
||||||
|
destination
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretSync };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Create ${
|
||||||
|
startsWithVowel(destinationName) ? "an" : "a"
|
||||||
|
} ${destinationName} Sync for the specified project environment.`,
|
||||||
|
body: createSchema,
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretSync: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const syncOptions = req.body.syncOptions ?? {};
|
||||||
|
|
||||||
|
const secretSync = (await server.services.secretSync.createSecretSync(
|
||||||
|
{ ...req.body, destination, syncOptions },
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: secretSync.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SECRET_SYNC,
|
||||||
|
metadata: {
|
||||||
|
syncId: secretSync.id,
|
||||||
|
destination,
|
||||||
|
...req.body,
|
||||||
|
syncOptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretSync };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:syncId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Update the specified ${destinationName} Connection.`,
|
||||||
|
params: z.object({
|
||||||
|
syncId: z.string().uuid().describe(SecretSyncs.UPDATE(destination).syncId)
|
||||||
|
}),
|
||||||
|
body: updateSchema,
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretSync: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { syncId } = req.params;
|
||||||
|
|
||||||
|
const secretSync = (await server.services.secretSync.updateSecretSync(
|
||||||
|
{ ...req.body, syncId, destination },
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: secretSync.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_SECRET_SYNC,
|
||||||
|
metadata: {
|
||||||
|
syncId,
|
||||||
|
destination,
|
||||||
|
...req.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretSync };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: `/:syncId`,
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Delete the specified ${destinationName} Connection.`,
|
||||||
|
params: z.object({
|
||||||
|
syncId: z.string().uuid().describe(SecretSyncs.DELETE(destination).syncId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretSync: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { syncId } = req.params;
|
||||||
|
|
||||||
|
const secretSync = (await server.services.secretSync.deleteSecretSync(
|
||||||
|
{ destination, syncId },
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_SECRET_SYNC,
|
||||||
|
metadata: {
|
||||||
|
destination,
|
||||||
|
syncId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretSync };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:syncId/sync",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Trigger a sync for the specified ${destinationName} Sync.`,
|
||||||
|
params: z.object({
|
||||||
|
syncId: z.string().uuid().describe(SecretSyncs.SYNC(destination).syncId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretSync: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { syncId } = req.params;
|
||||||
|
|
||||||
|
const secretSync = (await server.services.secretSync.triggerSecretSyncById(
|
||||||
|
{
|
||||||
|
syncId,
|
||||||
|
destination,
|
||||||
|
auditLogInfo: req.auditLogInfo
|
||||||
|
},
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
return { secretSync };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:syncId/import",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Import secrets from the specified ${destinationName} Sync destination.`,
|
||||||
|
params: z.object({
|
||||||
|
syncId: z.string().uuid().describe(SecretSyncs.IMPORT(destination).syncId)
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
shouldOverwrite: z
|
||||||
|
.enum(["true", "false"])
|
||||||
|
.optional()
|
||||||
|
.transform((val) => val === "true")
|
||||||
|
.describe(SecretSyncs.IMPORT(destination).shouldOverwrite)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretSync: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { syncId } = req.params;
|
||||||
|
const { shouldOverwrite } = req.query;
|
||||||
|
|
||||||
|
const secretSync = (await server.services.secretSync.triggerSecretSyncImportById(
|
||||||
|
{
|
||||||
|
syncId,
|
||||||
|
destination,
|
||||||
|
shouldOverwrite
|
||||||
|
},
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
return { secretSync };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:syncId/erase",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: `Erase synced secrets from the specified ${destinationName} Sync destination.`,
|
||||||
|
params: z.object({
|
||||||
|
syncId: z.string().uuid().describe(SecretSyncs.ERASE(destination).syncId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretSync: responseSchema })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { syncId } = req.params;
|
||||||
|
|
||||||
|
const secretSync = (await server.services.secretSync.triggerSecretSyncEraseById(
|
||||||
|
{
|
||||||
|
syncId,
|
||||||
|
destination
|
||||||
|
},
|
||||||
|
req.permission
|
||||||
|
)) as T;
|
||||||
|
|
||||||
|
return { secretSync };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,80 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { SecretSyncs } 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";
|
||||||
|
import {
|
||||||
|
AwsParameterStoreSyncListItemSchema,
|
||||||
|
AwsParameterStoreSyncSchema
|
||||||
|
} from "@app/services/secret-sync/aws-parameter-store";
|
||||||
|
|
||||||
|
// union once more available
|
||||||
|
const SecretSyncSchema = AwsParameterStoreSyncSchema;
|
||||||
|
|
||||||
|
// union once more available
|
||||||
|
const SecretSyncOptionsSchema = AwsParameterStoreSyncListItemSchema;
|
||||||
|
|
||||||
|
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/options",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List the available Secret Sync Options.",
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secretSyncOptions: SecretSyncOptionsSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: () => {
|
||||||
|
const secretSyncOptions = server.services.secretSync.listSecretSyncOptions();
|
||||||
|
return { secretSyncOptions };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List all the Secret Syncs for the specified project.",
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim().min(1, "Project ID required").describe(SecretSyncs.LIST().projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({ secretSyncs: SecretSyncSchema.array() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
query: { projectId },
|
||||||
|
permission
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
const secretSyncs = await server.services.secretSync.listSecretSyncsByProjectId({ projectId }, permission);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRET_SYNCS,
|
||||||
|
metadata: {
|
||||||
|
syncIds: secretSyncs.map((sync) => sync.id),
|
||||||
|
count: secretSyncs.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretSyncs };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -2,3 +2,50 @@ export enum AppConnection {
|
|||||||
GitHub = "github",
|
GitHub = "github",
|
||||||
AWS = "aws"
|
AWS = "aws"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AWSRegion {
|
||||||
|
// US
|
||||||
|
US_EAST_1 = "us-east-1", // N. Virginia
|
||||||
|
US_EAST_2 = "us-east-2", // Ohio
|
||||||
|
US_WEST_1 = "us-west-1", // N. California
|
||||||
|
US_WEST_2 = "us-west-2", // Oregon
|
||||||
|
|
||||||
|
// GovCloud
|
||||||
|
US_GOV_EAST_1 = "us-gov-east-1", // US-East
|
||||||
|
US_GOV_WEST_1 = "us-gov-west-1", // US-West
|
||||||
|
|
||||||
|
// Africa
|
||||||
|
AF_SOUTH_1 = "af-south-1", // Cape Town
|
||||||
|
|
||||||
|
// Asia Pacific
|
||||||
|
AP_EAST_1 = "ap-east-1", // Hong Kong
|
||||||
|
AP_SOUTH_1 = "ap-south-1", // Mumbai
|
||||||
|
AP_SOUTH_2 = "ap-south-2", // Hyderabad
|
||||||
|
AP_NORTHEAST_1 = "ap-northeast-1", // Tokyo
|
||||||
|
AP_NORTHEAST_2 = "ap-northeast-2", // Seoul
|
||||||
|
AP_NORTHEAST_3 = "ap-northeast-3", // Osaka
|
||||||
|
AP_SOUTHEAST_1 = "ap-southeast-1", // Singapore
|
||||||
|
AP_SOUTHEAST_2 = "ap-southeast-2", // Sydney
|
||||||
|
AP_SOUTHEAST_3 = "ap-southeast-3", // Jakarta
|
||||||
|
AP_SOUTHEAST_4 = "ap-southeast-4", // Melbourne
|
||||||
|
|
||||||
|
// Canada
|
||||||
|
CA_CENTRAL_1 = "ca-central-1", // Central
|
||||||
|
|
||||||
|
// Europe
|
||||||
|
EU_CENTRAL_1 = "eu-central-1", // Frankfurt
|
||||||
|
EU_CENTRAL_2 = "eu-central-2", // Zurich
|
||||||
|
EU_WEST_1 = "eu-west-1", // Ireland
|
||||||
|
EU_WEST_2 = "eu-west-2", // London
|
||||||
|
EU_WEST_3 = "eu-west-3", // Paris
|
||||||
|
EU_SOUTH_1 = "eu-south-1", // Milan
|
||||||
|
EU_SOUTH_2 = "eu-south-2", // Spain
|
||||||
|
EU_NORTH_1 = "eu-north-1", // Stockholm
|
||||||
|
|
||||||
|
// Middle East
|
||||||
|
ME_SOUTH_1 = "me-south-1", // Bahrain
|
||||||
|
ME_CENTRAL_1 = "me-central-1", // UAE
|
||||||
|
|
||||||
|
// South America
|
||||||
|
SA_EAST_1 = "sa-east-1" // Sao Paulo
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { TAppConnections } from "@app/db/schemas/app-connections";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
||||||
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
||||||
@ -64,9 +65,8 @@ export const validateAppConnectionCredentials = async (
|
|||||||
): Promise<TAppConnection["credentials"]> => {
|
): Promise<TAppConnection["credentials"]> => {
|
||||||
const { app } = appConnection;
|
const { app } = appConnection;
|
||||||
switch (app) {
|
switch (app) {
|
||||||
case AppConnection.AWS: {
|
case AppConnection.AWS:
|
||||||
return validateAwsConnectionCredentials(appConnection);
|
return validateAwsConnectionCredentials(appConnection);
|
||||||
}
|
|
||||||
case AppConnection.GitHub:
|
case AppConnection.GitHub:
|
||||||
return validateGitHubConnectionCredentials(appConnection);
|
return validateGitHubConnectionCredentials(appConnection);
|
||||||
default:
|
default:
|
||||||
@ -90,3 +90,17 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const decryptAppConnection = async (
|
||||||
|
appConnection: TAppConnections,
|
||||||
|
kmsService: TAppConnectionServiceFactoryDep["kmsService"]
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
...appConnection,
|
||||||
|
credentials: await decryptAppConnectionCredentials({
|
||||||
|
encryptedCredentials: appConnection.encryptedCredentials,
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
})
|
||||||
|
} as TAppConnection;
|
||||||
|
};
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import {
|
import {
|
||||||
decryptAppConnectionCredentials,
|
decryptAppConnection,
|
||||||
encryptAppConnectionCredentials,
|
encryptAppConnectionCredentials,
|
||||||
getAppConnectionMethodName,
|
getAppConnectionMethodName,
|
||||||
listAppConnectionOptions,
|
listAppConnectionOptions,
|
||||||
@ -65,7 +65,10 @@ export const appConnectionServiceFactory = ({
|
|||||||
actor.orgId
|
actor.orgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionAppConnectionActions.Read,
|
||||||
|
OrgPermissionSubjects.AppConnections
|
||||||
|
);
|
||||||
|
|
||||||
const appConnections = await appConnectionDAL.find(
|
const appConnections = await appConnectionDAL.find(
|
||||||
app
|
app
|
||||||
@ -78,18 +81,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
return Promise.all(
|
return Promise.all(
|
||||||
appConnections
|
appConnections
|
||||||
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
||||||
.map(async ({ encryptedCredentials, ...connection }) => {
|
.map((appConnection) => decryptAppConnection(appConnection, kmsService))
|
||||||
const credentials = await decryptAppConnectionCredentials({
|
|
||||||
encryptedCredentials,
|
|
||||||
kmsService,
|
|
||||||
orgId: connection.orgId
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...connection,
|
|
||||||
credentials
|
|
||||||
} as TAppConnection;
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -108,19 +100,15 @@ export const appConnectionServiceFactory = ({
|
|||||||
appConnection.orgId
|
appConnection.orgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionAppConnectionActions.Read,
|
||||||
|
OrgPermissionSubjects.AppConnections
|
||||||
|
);
|
||||||
|
|
||||||
if (appConnection.app !== app)
|
if (appConnection.app !== app)
|
||||||
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
||||||
|
|
||||||
return {
|
return decryptAppConnection(appConnection, kmsService);
|
||||||
...appConnection,
|
|
||||||
credentials: await decryptAppConnectionCredentials({
|
|
||||||
encryptedCredentials: appConnection.encryptedCredentials,
|
|
||||||
orgId: appConnection.orgId,
|
|
||||||
kmsService
|
|
||||||
})
|
|
||||||
} as TAppConnection;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const findAppConnectionByName = async (app: AppConnection, connectionName: string, actor: OrgServiceActor) => {
|
const findAppConnectionByName = async (app: AppConnection, connectionName: string, actor: OrgServiceActor) => {
|
||||||
@ -139,19 +127,15 @@ export const appConnectionServiceFactory = ({
|
|||||||
appConnection.orgId
|
appConnection.orgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AppConnections);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionAppConnectionActions.Read,
|
||||||
|
OrgPermissionSubjects.AppConnections
|
||||||
|
);
|
||||||
|
|
||||||
if (appConnection.app !== app)
|
if (appConnection.app !== app)
|
||||||
throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for App "${app}"` });
|
throw new BadRequestError({ message: `App Connection with name ${connectionName} is not for App "${app}"` });
|
||||||
|
|
||||||
return {
|
return decryptAppConnection(appConnection, kmsService);
|
||||||
...appConnection,
|
|
||||||
credentials: await decryptAppConnectionCredentials({
|
|
||||||
encryptedCredentials: appConnection.encryptedCredentials,
|
|
||||||
orgId: appConnection.orgId,
|
|
||||||
kmsService
|
|
||||||
})
|
|
||||||
} as TAppConnection;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createAppConnection = async (
|
const createAppConnection = async (
|
||||||
@ -168,7 +152,10 @@ export const appConnectionServiceFactory = ({
|
|||||||
actor.orgId
|
actor.orgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionAppConnectionActions.Create,
|
||||||
|
OrgPermissionSubjects.AppConnections
|
||||||
|
);
|
||||||
|
|
||||||
const appConnection = await appConnectionDAL.transaction(async (tx) => {
|
const appConnection = await appConnectionDAL.transaction(async (tx) => {
|
||||||
const isConflictingName = Boolean(
|
const isConflictingName = Boolean(
|
||||||
@ -216,7 +203,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return appConnection;
|
return appConnection as TAppConnection;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAppConnection = async (
|
const updateAppConnection = async (
|
||||||
@ -237,7 +224,10 @@ export const appConnectionServiceFactory = ({
|
|||||||
appConnection.orgId
|
appConnection.orgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionAppConnectionActions.Edit,
|
||||||
|
OrgPermissionSubjects.AppConnections
|
||||||
|
);
|
||||||
|
|
||||||
const updatedAppConnection = await appConnectionDAL.transaction(async (tx) => {
|
const updatedAppConnection = await appConnectionDAL.transaction(async (tx) => {
|
||||||
if (params.name && appConnection.name !== params.name) {
|
if (params.name && appConnection.name !== params.name) {
|
||||||
@ -304,14 +294,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
return updatedConnection;
|
return updatedConnection;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return decryptAppConnection(updatedAppConnection, kmsService);
|
||||||
...updatedAppConnection,
|
|
||||||
credentials: await decryptAppConnectionCredentials({
|
|
||||||
encryptedCredentials: updatedAppConnection.encryptedCredentials,
|
|
||||||
orgId: updatedAppConnection.orgId,
|
|
||||||
kmsService
|
|
||||||
})
|
|
||||||
} as TAppConnection;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteAppConnection = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
|
const deleteAppConnection = async (app: AppConnection, connectionId: string, actor: OrgServiceActor) => {
|
||||||
@ -329,23 +312,74 @@ export const appConnectionServiceFactory = ({
|
|||||||
appConnection.orgId
|
appConnection.orgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.AppConnections);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionAppConnectionActions.Delete,
|
||||||
|
OrgPermissionSubjects.AppConnections
|
||||||
|
);
|
||||||
|
|
||||||
if (appConnection.app !== app)
|
if (appConnection.app !== app)
|
||||||
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
throw new BadRequestError({ message: `App Connection with ID ${connectionId} is not for App "${app}"` });
|
||||||
|
|
||||||
// TODO: specify delete error message if due to existing dependencies
|
// TODO (scott): add option to delete all dependencies
|
||||||
|
|
||||||
|
try {
|
||||||
const deletedAppConnection = await appConnectionDAL.deleteById(connectionId);
|
const deletedAppConnection = await appConnectionDAL.deleteById(connectionId);
|
||||||
|
|
||||||
return {
|
return await decryptAppConnection(deletedAppConnection, kmsService);
|
||||||
...deletedAppConnection,
|
} catch (err) {
|
||||||
credentials: await decryptAppConnectionCredentials({
|
if (err instanceof DatabaseError && (err.error as { code: string })?.code === "23503") {
|
||||||
encryptedCredentials: deletedAppConnection.encryptedCredentials,
|
throw new BadRequestError({
|
||||||
orgId: deletedAppConnection.orgId,
|
message:
|
||||||
kmsService
|
"Cannot delete App Connection with existing connections. Remove all existing connections and try again."
|
||||||
})
|
});
|
||||||
} as TAppConnection;
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectAppConnectionById = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
|
if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` });
|
||||||
|
|
||||||
|
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
appConnection.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(orgPermission).throwUnlessCan(
|
||||||
|
OrgPermissionAppConnectionActions.Connect,
|
||||||
|
subject(OrgPermissionSubjects.AppConnections, { connectionId: appConnection.id })
|
||||||
|
);
|
||||||
|
|
||||||
|
return decryptAppConnection(appConnection, kmsService);
|
||||||
|
};
|
||||||
|
|
||||||
|
const listAvailableAppConnectionsForUser = async (app: AppConnection, actor: OrgServiceActor) => {
|
||||||
|
await checkAppServicesAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
const appConnections = await appConnectionDAL.find({ app, orgId: actor.orgId });
|
||||||
|
|
||||||
|
const availableConnections = appConnections.filter((connection) =>
|
||||||
|
orgPermission.can(
|
||||||
|
OrgPermissionAppConnectionActions.Connect,
|
||||||
|
subject(OrgPermissionSubjects.AppConnections, { connectionId: connection.id })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return availableConnections as Omit<TAppConnection, "credentials">[];
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -355,6 +389,8 @@ export const appConnectionServiceFactory = ({
|
|||||||
findAppConnectionByName,
|
findAppConnectionByName,
|
||||||
createAppConnection,
|
createAppConnection,
|
||||||
updateAppConnection,
|
updateAppConnection,
|
||||||
deleteAppConnection
|
deleteAppConnection,
|
||||||
|
connectAppConnectionById,
|
||||||
|
listAvailableAppConnectionsForUser
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import { randomUUID } from "crypto";
|
|||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
import { AwsConnectionMethod } from "./aws-connection-enums";
|
import { AwsConnectionMethod } from "./aws-connection-enums";
|
||||||
import { TAwsConnectionConfig } from "./aws-connection-types";
|
import { TAwsConnectionConfig } from "./aws-connection-types";
|
||||||
@ -20,7 +20,7 @@ export const getAwsAppConnectionListItem = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAwsConnectionConfig = async (appConnection: TAwsConnectionConfig, region = "us-east-1") => {
|
export const getAwsConnectionConfig = async (appConnection: TAwsConnectionConfig, region = AWSRegion.US_EAST_1) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
let accessKeyId: string;
|
let accessKeyId: string;
|
||||||
|
@ -75,7 +75,7 @@ export const UpdateAwsConnectionSchema = z
|
|||||||
export const AwsConnectionListItemSchema = z.object({
|
export const AwsConnectionListItemSchema = z.object({
|
||||||
name: z.literal("AWS"),
|
name: z.literal("AWS"),
|
||||||
app: z.literal(AppConnection.AWS),
|
app: z.literal(AppConnection.AWS),
|
||||||
// the below is preferable but currently breaks mintlify
|
// the below is preferable but currently breaks with our zod to json schema parser
|
||||||
// methods: z.tuple([z.literal(AwsConnectionMethod.AssumeRole), z.literal(AwsConnectionMethod.AccessKey)]),
|
// methods: z.tuple([z.literal(AwsConnectionMethod.AssumeRole), z.literal(AwsConnectionMethod.AccessKey)]),
|
||||||
methods: z.nativeEnum(AwsConnectionMethod).array(),
|
methods: z.nativeEnum(AwsConnectionMethod).array(),
|
||||||
accessKeyId: z.string().optional()
|
accessKeyId: z.string().optional()
|
||||||
|
@ -57,7 +57,7 @@ export const UpdateGitHubConnectionSchema = z
|
|||||||
|
|
||||||
const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) });
|
const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) });
|
||||||
|
|
||||||
export const GitHubAppConnectionSchema = z.intersection(
|
export const GitHubConnectionSchema = z.intersection(
|
||||||
BaseGitHubConnectionSchema,
|
BaseGitHubConnectionSchema,
|
||||||
z.discriminatedUnion("method", [
|
z.discriminatedUnion("method", [
|
||||||
z.object({
|
z.object({
|
||||||
@ -85,8 +85,8 @@ export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
|
|||||||
export const GitHubConnectionListItemSchema = z.object({
|
export const GitHubConnectionListItemSchema = z.object({
|
||||||
name: z.literal("GitHub"),
|
name: z.literal("GitHub"),
|
||||||
app: z.literal(AppConnection.GitHub),
|
app: z.literal(AppConnection.GitHub),
|
||||||
// the below is preferable but currently breaks mintlify
|
// the below is preferable but currently breaks with our zod to json schema parser
|
||||||
// methods: z.tuple([z.literal(GitHubConnectionMethod.GitHubApp), z.literal(GitHubConnectionMethod.OAuth)]),
|
// methods: z.tuple([z.literal(GitHubConnectionMethod.App), z.literal(GitHubConnectionMethod.OAuth)]),
|
||||||
methods: z.nativeEnum(GitHubConnectionMethod).array(),
|
methods: z.nativeEnum(GitHubConnectionMethod).array(),
|
||||||
oauthClientId: z.string().optional(),
|
oauthClientId: z.string().optional(),
|
||||||
appClientSlug: z.string().optional()
|
appClientSlug: z.string().optional()
|
||||||
|
@ -5,11 +5,11 @@ import { DiscriminativePick } from "@app/lib/types";
|
|||||||
import { AppConnection } from "../app-connection-enums";
|
import { AppConnection } from "../app-connection-enums";
|
||||||
import {
|
import {
|
||||||
CreateGitHubConnectionSchema,
|
CreateGitHubConnectionSchema,
|
||||||
GitHubAppConnectionSchema,
|
GitHubConnectionSchema,
|
||||||
ValidateGitHubConnectionCredentialsSchema
|
ValidateGitHubConnectionCredentialsSchema
|
||||||
} from "./github-connection-schemas";
|
} from "./github-connection-schemas";
|
||||||
|
|
||||||
export type TGitHubConnection = z.infer<typeof GitHubAppConnectionSchema>;
|
export type TGitHubConnection = z.infer<typeof GitHubConnectionSchema>;
|
||||||
|
|
||||||
export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema> & {
|
export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema> & {
|
||||||
app: AppConnection.GitHub;
|
app: AppConnection.GitHub;
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
export const AWS_PARAMETER_STORE_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||||
|
name: "AWS Parameter Store",
|
||||||
|
destination: SecretSync.AWSParameterStore,
|
||||||
|
connection: AppConnection.AWS,
|
||||||
|
supportsImport: true
|
||||||
|
};
|
@ -0,0 +1,198 @@
|
|||||||
|
import AWS, { AWSError } from "aws-sdk";
|
||||||
|
|
||||||
|
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||||
|
import { TSecretMap, TSecretSyncWithConnection } from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
import { TAwsParameterStoreSyncWithConnection } from "./aws-parameter-store-sync-types";
|
||||||
|
|
||||||
|
type TAWSParameterStoreRecord = Record<string, AWS.SSM.Parameter>;
|
||||||
|
|
||||||
|
const MAX_RETRIES = 5;
|
||||||
|
const BATCH_SIZE = 10;
|
||||||
|
|
||||||
|
const getSSM = async (secretSync: TSecretSyncWithConnection) => {
|
||||||
|
const { destinationConfig, connection } = secretSync;
|
||||||
|
|
||||||
|
const config = await getAwsConnectionConfig(connection, destinationConfig.region);
|
||||||
|
|
||||||
|
const ssm = new AWS.SSM({
|
||||||
|
apiVersion: "2014-11-06",
|
||||||
|
region: destinationConfig.region
|
||||||
|
});
|
||||||
|
|
||||||
|
ssm.config.update(config);
|
||||||
|
|
||||||
|
return ssm;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sleep = async () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSParameterStoreRecord> => {
|
||||||
|
const awsParameterStoreSecretsRecord: TAWSParameterStoreRecord = {};
|
||||||
|
let hasNext = true;
|
||||||
|
let nextToken: string | undefined;
|
||||||
|
let attempt = 0;
|
||||||
|
|
||||||
|
while (hasNext) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const parameters = await ssm
|
||||||
|
.getParametersByPath({
|
||||||
|
Path: path,
|
||||||
|
Recursive: false,
|
||||||
|
WithDecryption: true,
|
||||||
|
MaxResults: BATCH_SIZE,
|
||||||
|
NextToken: nextToken
|
||||||
|
})
|
||||||
|
.promise();
|
||||||
|
|
||||||
|
attempt = 0;
|
||||||
|
|
||||||
|
if (parameters.Parameters) {
|
||||||
|
parameters.Parameters.forEach((parameter) => {
|
||||||
|
if (parameter.Name) {
|
||||||
|
const secKey = parameter.Name.substring(path.length);
|
||||||
|
awsParameterStoreSecretsRecord[secKey] = parameter;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNext = Boolean(parameters.NextToken);
|
||||||
|
nextToken = parameters.NextToken;
|
||||||
|
} catch (e) {
|
||||||
|
if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
attempt += 1;
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await sleep();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return awsParameterStoreSecretsRecord;
|
||||||
|
};
|
||||||
|
|
||||||
|
const putParameter = async (
|
||||||
|
ssm: AWS.SSM,
|
||||||
|
params: AWS.SSM.PutParameterRequest,
|
||||||
|
attempt = 0
|
||||||
|
): Promise<AWS.SSM.PutParameterResult> => {
|
||||||
|
try {
|
||||||
|
return await ssm.putParameter(params).promise();
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
// retry
|
||||||
|
return putParameter(ssm, params, attempt + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteParametersBatch = async (
|
||||||
|
ssm: AWS.SSM,
|
||||||
|
parameters: AWS.SSM.Parameter[],
|
||||||
|
attempt = 0
|
||||||
|
): Promise<AWS.SSM.DeleteParameterResult[]> => {
|
||||||
|
const results: AWS.SSM.DeleteParameterResult[] = [];
|
||||||
|
let remainingParams = [...parameters];
|
||||||
|
|
||||||
|
while (remainingParams.length > 0) {
|
||||||
|
const batch = remainingParams.slice(0, BATCH_SIZE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const result = await ssm.deleteParameters({ Names: batch.map((param) => param.Name!) }).promise();
|
||||||
|
results.push(result);
|
||||||
|
remainingParams = remainingParams.slice(BATCH_SIZE);
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
// Retry the current batch
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
return [...results, ...(await deleteParametersBatch(ssm, remainingParams, attempt + 1))];
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AwsParameterStoreSyncFns = {
|
||||||
|
sync: async (secretSync: TAwsParameterStoreSyncWithConnection, secrets: TSecretMap) => {
|
||||||
|
const { destinationConfig } = secretSync;
|
||||||
|
|
||||||
|
const ssm = await getSSM(secretSync);
|
||||||
|
|
||||||
|
// TODO(scott): KMS Key ID, Tags
|
||||||
|
|
||||||
|
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||||
|
|
||||||
|
for await (const entry of Object.entries(secrets)) {
|
||||||
|
const [key, { value }] = entry;
|
||||||
|
|
||||||
|
// skip empty values (not allowed by AWS) or secrets that haven't changed
|
||||||
|
if (!value || (key in awsParameterStoreSecretsRecord && awsParameterStoreSecretsRecord[key].Value === value)) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await putParameter(ssm, {
|
||||||
|
Name: `${destinationConfig.path}${key}`,
|
||||||
|
Type: "SecureString",
|
||||||
|
Value: value,
|
||||||
|
Overwrite: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const parametersToDelete: AWS.SSM.Parameter[] = [];
|
||||||
|
|
||||||
|
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
|
||||||
|
const [key, parameter] = entry;
|
||||||
|
|
||||||
|
if (!(key in secrets) || !secrets[key].value) {
|
||||||
|
parametersToDelete.push(parameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteParametersBatch(ssm, parametersToDelete);
|
||||||
|
},
|
||||||
|
import: async (secretSync: TAwsParameterStoreSyncWithConnection): Promise<TSecretMap> => {
|
||||||
|
const { destinationConfig } = secretSync;
|
||||||
|
|
||||||
|
const ssm = await getSSM(secretSync);
|
||||||
|
|
||||||
|
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(awsParameterStoreSecretsRecord).map(([key, value]) => [key, { value: value.Value ?? "" }])
|
||||||
|
);
|
||||||
|
},
|
||||||
|
erase: async (secretSync: TAwsParameterStoreSyncWithConnection, secrets: TSecretMap) => {
|
||||||
|
const { destinationConfig } = secretSync;
|
||||||
|
|
||||||
|
const ssm = await getSSM(secretSync);
|
||||||
|
|
||||||
|
const awsParameterStoreSecretsRecord = await getParametersByPath(ssm, destinationConfig.path);
|
||||||
|
|
||||||
|
const parametersToDelete: AWS.SSM.Parameter[] = [];
|
||||||
|
|
||||||
|
for (const entry of Object.entries(awsParameterStoreSecretsRecord)) {
|
||||||
|
const [key, param] = entry;
|
||||||
|
|
||||||
|
if (key in secrets) {
|
||||||
|
parametersToDelete.push(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteParametersBatch(ssm, parametersToDelete);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,59 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretSyncs } from "@app/lib/api-docs";
|
||||||
|
import { wrapWithSlashes } from "@app/lib/fn";
|
||||||
|
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import {
|
||||||
|
BaseSecretSyncSchema,
|
||||||
|
GenericCreateSecretSyncFieldsSchema,
|
||||||
|
GenericUpdateSecretSyncFieldsSchema
|
||||||
|
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||||
|
|
||||||
|
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||||
|
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.REGION),
|
||||||
|
path: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Parameter Store Path Required")
|
||||||
|
.transform(wrapWithSlashes)
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (!/^\/([\w-]+\/)*[\w-]+\/$/.test(val)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `Invalid Parameter Store Path - must follow "/example/path/" format`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val.length > 2048) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `Invalid Parameter Store Path - cannot exceed 2048 characters`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.PATH)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(AppConnection.AWS).extend({
|
||||||
|
destination: z.literal(SecretSync.AWSParameterStore),
|
||||||
|
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateAwsParameterStoreSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.AWSParameterStore
|
||||||
|
).extend({
|
||||||
|
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateAwsParameterStoreSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||||
|
SecretSync.AWSParameterStore
|
||||||
|
).extend({
|
||||||
|
destinationConfig: AwsParameterStoreSyncDestinationConfigSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AwsParameterStoreSyncListItemSchema = z.object({
|
||||||
|
name: z.literal("AWS Parameter Store"),
|
||||||
|
connection: z.literal(AppConnection.AWS),
|
||||||
|
destination: z.literal(SecretSync.AWSParameterStore),
|
||||||
|
supportsImport: z.literal(true)
|
||||||
|
});
|
@ -0,0 +1,19 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TAwsConnection } from "@app/services/app-connection/aws";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AwsParameterStoreSyncListItemSchema,
|
||||||
|
AwsParameterStoreSyncSchema,
|
||||||
|
CreateAwsParameterStoreSyncSchema
|
||||||
|
} from "./aws-parameter-store-sync-schemas";
|
||||||
|
|
||||||
|
export type TAwsParameterStoreSync = z.infer<typeof AwsParameterStoreSyncSchema>;
|
||||||
|
|
||||||
|
export type TAwsParameterStoreSyncInput = z.infer<typeof CreateAwsParameterStoreSyncSchema>;
|
||||||
|
|
||||||
|
export type TAwsParameterStoreSyncListItem = z.infer<typeof AwsParameterStoreSyncListItemSchema>;
|
||||||
|
|
||||||
|
export type TAwsParameterStoreSyncWithConnection = Omit<TAwsParameterStoreSync, "connection"> & {
|
||||||
|
connection: TAwsConnection;
|
||||||
|
};
|
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./aws-parameter-store-sync-constants";
|
||||||
|
export * from "./aws-parameter-store-sync-fns";
|
||||||
|
export * from "./aws-parameter-store-sync-schemas";
|
||||||
|
export * from "./aws-parameter-store-sync-types";
|
201
backend/src/services/secret-sync/secret-sync-dal.ts
Normal file
201
backend/src/services/secret-sync/secret-sync-dal.ts
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { TSecretSyncs } from "@app/db/schemas/secret-syncs";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
|
||||||
|
export type TSecretSyncDALFactory = ReturnType<typeof secretSyncDALFactory>;
|
||||||
|
|
||||||
|
type SecretSyncFindFilter = Parameters<typeof buildFindFilter<TSecretSyncs>>[0];
|
||||||
|
|
||||||
|
const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: SecretSyncFindFilter; tx?: Knex }) => {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.SecretSync)
|
||||||
|
.join(TableName.SecretFolder, `${TableName.SecretSync}.folderId`, `${TableName.SecretFolder}.id`)
|
||||||
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.join(TableName.AppConnection, `${TableName.SecretSync}.connectionId`, `${TableName.AppConnection}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.SecretSync))
|
||||||
|
.select(
|
||||||
|
// evironment
|
||||||
|
db.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||||
|
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||||
|
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
|
db.ref("projectId").withSchema(TableName.Environment),
|
||||||
|
// 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")
|
||||||
|
);
|
||||||
|
|
||||||
|
// prepends table name to filter keys to avoid ambiguous col references, skipping utility filters like $in, etc.
|
||||||
|
const prependTableName = (filterObj: object): SecretSyncFindFilter =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(filterObj).map(([key, value]) =>
|
||||||
|
key.startsWith("$") ? [key, prependTableName(value as object)] : [`${TableName.SecretSync}.${key}`, value]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||||
|
void query.where(buildFindFilter(prependTableName(filter)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandSecretSync = (
|
||||||
|
secretSync: Awaited<ReturnType<typeof baseSecretSyncQuery>>[number],
|
||||||
|
folder: Awaited<ReturnType<TSecretFolderDALFactory["findSecretPathByFolderIds"]>>[number]
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
envId,
|
||||||
|
envName,
|
||||||
|
envSlug,
|
||||||
|
connectionApp,
|
||||||
|
connectionName,
|
||||||
|
connectionId,
|
||||||
|
connectionOrgId,
|
||||||
|
connectionEncryptedCredentials,
|
||||||
|
connectionMethod,
|
||||||
|
connectionDescription,
|
||||||
|
connectionCreatedAt,
|
||||||
|
connectionUpdatedAt,
|
||||||
|
connectionVersion,
|
||||||
|
...el
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...el,
|
||||||
|
connectionId,
|
||||||
|
environment: { id: envId, name: envName, slug: envSlug },
|
||||||
|
connection: {
|
||||||
|
app: connectionApp,
|
||||||
|
id: connectionId,
|
||||||
|
name: connectionName,
|
||||||
|
orgId: connectionOrgId,
|
||||||
|
encryptedCredentials: connectionEncryptedCredentials,
|
||||||
|
method: connectionMethod,
|
||||||
|
description: connectionDescription,
|
||||||
|
createdAt: connectionCreatedAt,
|
||||||
|
updatedAt: connectionUpdatedAt,
|
||||||
|
version: connectionVersion
|
||||||
|
},
|
||||||
|
folder: {
|
||||||
|
id: folder!.id,
|
||||||
|
path: folder!.path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const secretSyncDALFactory = (
|
||||||
|
db: TDbClient,
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds">
|
||||||
|
) => {
|
||||||
|
const secretSyncOrm = ormify(db, TableName.SecretSync);
|
||||||
|
|
||||||
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const secretSync = await baseSecretSyncQuery({
|
||||||
|
filter: { id },
|
||||||
|
db,
|
||||||
|
tx
|
||||||
|
}).first();
|
||||||
|
|
||||||
|
if (secretSync) {
|
||||||
|
// TODO (scott): replace with cached folder path once implemented
|
||||||
|
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]);
|
||||||
|
return expandSecretSync(secretSync, folderWithPath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find by ID - Secret Sync" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (data: Parameters<(typeof secretSyncOrm)["create"]>[0]) => {
|
||||||
|
try {
|
||||||
|
const secretSync = (await secretSyncOrm.transaction(async (tx) => {
|
||||||
|
const sync = await secretSyncOrm.create(data, tx);
|
||||||
|
|
||||||
|
return baseSecretSyncQuery({
|
||||||
|
filter: { id: sync.id },
|
||||||
|
db,
|
||||||
|
tx
|
||||||
|
}).first();
|
||||||
|
}))!;
|
||||||
|
|
||||||
|
// TODO (scott): replace with cached folder path once implemented
|
||||||
|
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]);
|
||||||
|
return expandSecretSync(secretSync, folderWithPath);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Create - Secret Sync" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateById = async (syncId: string, data: Parameters<(typeof secretSyncOrm)["updateById"]>[1]) => {
|
||||||
|
try {
|
||||||
|
const secretSync = (await secretSyncOrm.transaction(async (tx) => {
|
||||||
|
const sync = await secretSyncOrm.updateById(syncId, data, tx);
|
||||||
|
|
||||||
|
return baseSecretSyncQuery({
|
||||||
|
filter: { id: sync.id },
|
||||||
|
db,
|
||||||
|
tx
|
||||||
|
}).first();
|
||||||
|
}))!;
|
||||||
|
|
||||||
|
// TODO (scott): replace with cached folder path once implemented
|
||||||
|
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]);
|
||||||
|
return expandSecretSync(secretSync, folderWithPath);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Update by ID - Secret Sync" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findOne = async (filter: Parameters<(typeof secretSyncOrm)["findOne"]>[0], tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const secretSync = await baseSecretSyncQuery({ filter, db, tx }).first();
|
||||||
|
|
||||||
|
if (secretSync) {
|
||||||
|
// TODO (scott): replace with cached folder path once implemented
|
||||||
|
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(secretSync.projectId, [secretSync.folderId]);
|
||||||
|
return expandSecretSync(secretSync, folderWithPath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find One - Secret Sync" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const find = async (filter: Parameters<(typeof secretSyncOrm)["find"]>[0], tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const secretSyncs = await baseSecretSyncQuery({ filter, db, tx });
|
||||||
|
|
||||||
|
if (!secretSyncs.length) return [];
|
||||||
|
|
||||||
|
const foldersWithPath = await folderDAL.findSecretPathByFolderIds(
|
||||||
|
secretSyncs[0].projectId,
|
||||||
|
secretSyncs.map((sync) => sync.folderId)
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO (scott): replace with cached folder path once implemented
|
||||||
|
const folderRecord: Record<string, (typeof foldersWithPath)[number]> = {};
|
||||||
|
|
||||||
|
foldersWithPath.forEach((folder) => {
|
||||||
|
if (folder) folderRecord[folder.id] = folder;
|
||||||
|
});
|
||||||
|
|
||||||
|
return secretSyncs.map((secretSync) => expandSecretSync(secretSync, folderRecord[secretSync.folderId]));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find - Secret Sync" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...secretSyncOrm, findById, findOne, find, create, updateById };
|
||||||
|
};
|
3
backend/src/services/secret-sync/secret-sync-enums.ts
Normal file
3
backend/src/services/secret-sync/secret-sync-enums.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export enum SecretSync {
|
||||||
|
AWSParameterStore = "aws-parameter-store"
|
||||||
|
}
|
70
backend/src/services/secret-sync/secret-sync-fns.ts
Normal file
70
backend/src/services/secret-sync/secret-sync-fns.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import {
|
||||||
|
AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
||||||
|
AwsParameterStoreSyncFns
|
||||||
|
} from "@app/services/secret-sync/aws-parameter-store";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
||||||
|
import {
|
||||||
|
TSecretMap,
|
||||||
|
TSecretSyncListItem,
|
||||||
|
TSecretSyncWithConnection
|
||||||
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||||
|
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listSecretSyncOptions = () => {
|
||||||
|
return Object.values(SECRET_SYNC_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
const processSyncOptions = (secretSync: TSecretSyncWithConnection, 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretSyncFns = {
|
||||||
|
sync: (secretSync: TSecretSyncWithConnection, unprocessedSecretMap: TSecretMap): Promise<void> => {
|
||||||
|
const secretMap = processSyncOptions(secretSync, unprocessedSecretMap);
|
||||||
|
|
||||||
|
switch (secretSync.destination) {
|
||||||
|
case SecretSync.AWSParameterStore:
|
||||||
|
return AwsParameterStoreSyncFns.sync(secretSync, secretMap);
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
throw new Error(`Unhandled sync destination for push secrets: ${secretSync.destination}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
import: (secretSync: TSecretSyncWithConnection): Promise<TSecretMap> => {
|
||||||
|
switch (secretSync.destination) {
|
||||||
|
case SecretSync.AWSParameterStore:
|
||||||
|
return AwsParameterStoreSyncFns.import(secretSync);
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `${SECRET_SYNC_NAME_MAP[secretSync.destination as SecretSync]} Syncs do not support pulling.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
erase: (secretSync: TSecretSyncWithConnection, unprocessedSecretMap: TSecretMap): Promise<void> => {
|
||||||
|
const secretMap = processSyncOptions(secretSync, unprocessedSecretMap);
|
||||||
|
|
||||||
|
switch (secretSync.destination) {
|
||||||
|
case SecretSync.AWSParameterStore:
|
||||||
|
return AwsParameterStoreSyncFns.erase(secretSync, secretMap);
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
throw new Error(`Unhandled sync destination for purging secrets: ${secretSync.destination}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
10
backend/src/services/secret-sync/secret-sync-maps.ts
Normal file
10
backend/src/services/secret-sync/secret-sync-maps.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
|
export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||||
|
[SecretSync.AWSParameterStore]: "AWS Parameter Store"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||||
|
[SecretSync.AWSParameterStore]: AppConnection.AWS
|
||||||
|
};
|
809
backend/src/services/secret-sync/secret-sync-queue.ts
Normal file
809
backend/src/services/secret-sync/secret-sync-queue.ts
Normal file
@ -0,0 +1,809 @@
|
|||||||
|
import opentelemetry from "@opentelemetry/api";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
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 { InternalServerError } from "@app/lib/errors";
|
||||||
|
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 { 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 } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
import { 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,
|
||||||
|
TQueueSecretSyncByIdDTO,
|
||||||
|
TQueueSecretSyncEraseByIdDTO,
|
||||||
|
TQueueSecretSyncImportByIdDTO,
|
||||||
|
TQueueSecretSyncsByPathDTO,
|
||||||
|
TQueueSendSecretSyncActionFailedNotificationsDTO,
|
||||||
|
TSecretMap,
|
||||||
|
TSecretSyncDTO,
|
||||||
|
TSecretSyncEraseDTO,
|
||||||
|
TSecretSyncImportDTO,
|
||||||
|
TSecretSyncRaw,
|
||||||
|
TSecretSyncWithConnection,
|
||||||
|
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";
|
||||||
|
|
||||||
|
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
||||||
|
|
||||||
|
type TSecretSyncQueueFactoryDep = {
|
||||||
|
queueService: Pick<TQueueServiceFactory, "queue" | "start">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
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">;
|
||||||
|
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">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const secretSyncQueueFactory = ({
|
||||||
|
queueService,
|
||||||
|
kmsService,
|
||||||
|
keyStore,
|
||||||
|
folderDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
secretImportDAL,
|
||||||
|
secretSyncDAL,
|
||||||
|
auditLogService,
|
||||||
|
projectMembershipDAL,
|
||||||
|
projectDAL,
|
||||||
|
smtpService,
|
||||||
|
projectBotDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretBlindIndexDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagDAL,
|
||||||
|
secretVersionV2BridgeDAL,
|
||||||
|
secretVersionTagV2BridgeDAL
|
||||||
|
}: TSecretSyncQueueFactoryDep) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const integrationMeter = opentelemetry.metrics.getMeter("SecretSyncs");
|
||||||
|
const syncErrorHistogram = integrationMeter.createHistogram("secret_sync_errors", {
|
||||||
|
description: "Secret Sync - sync errors",
|
||||||
|
unit: "1"
|
||||||
|
});
|
||||||
|
const importErrorHistogram = integrationMeter.createHistogram("secret_sync_import_errors", {
|
||||||
|
description: "Secret Sync - import errors",
|
||||||
|
unit: "1"
|
||||||
|
});
|
||||||
|
const eraseErrorHistogram = integrationMeter.createHistogram("secret_sync_erase_errors", {
|
||||||
|
description: "Secret Sync - erase errors",
|
||||||
|
unit: "1"
|
||||||
|
});
|
||||||
|
|
||||||
|
const $createManySecretsRawFn = createManySecretsRawFnFactory({
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretBlindIndexDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagDAL,
|
||||||
|
folderDAL,
|
||||||
|
kmsService,
|
||||||
|
secretVersionV2BridgeDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
secretVersionTagV2BridgeDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const $updateManySecretsRawFn = updateManySecretsRawFnFactory({
|
||||||
|
projectDAL,
|
||||||
|
projectBotDAL,
|
||||||
|
secretDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretBlindIndexDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagDAL,
|
||||||
|
folderDAL,
|
||||||
|
kmsService,
|
||||||
|
secretVersionV2BridgeDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
secretVersionTagV2BridgeDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const $getSecrets = async (secretSync: TSecretSyncRaw, includeImports = true) => {
|
||||||
|
const {
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
environment: { slug: environmentSlug },
|
||||||
|
folder: { path: secretPath }
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
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: environmentSlug,
|
||||||
|
secretPath,
|
||||||
|
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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
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 || ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return secretMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const queueSecretSyncById = async (payload: TQueueSecretSyncByIdDTO) =>
|
||||||
|
queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.AppConnectionSecretSync, payload, {
|
||||||
|
attempts: 5,
|
||||||
|
delay: 1000,
|
||||||
|
backoff: {
|
||||||
|
type: "exponential",
|
||||||
|
delay: 3000
|
||||||
|
},
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const queueSecretSyncImportById = async (payload: TQueueSecretSyncImportByIdDTO) =>
|
||||||
|
queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.AppConnectionSecretSyncImport, payload, {
|
||||||
|
attempts: 5,
|
||||||
|
delay: 1000,
|
||||||
|
backoff: {
|
||||||
|
type: "exponential",
|
||||||
|
delay: 3000
|
||||||
|
},
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const queueSecretSyncEraseById = async (payload: TQueueSecretSyncEraseByIdDTO) =>
|
||||||
|
queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.AppConnectionSecretSyncErase, payload, {
|
||||||
|
attempts: 5,
|
||||||
|
delay: 1000,
|
||||||
|
backoff: {
|
||||||
|
type: "exponential",
|
||||||
|
delay: 3000
|
||||||
|
},
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const $queueSendSecretSyncFailedNotifications = async (payload: TQueueSendSecretSyncActionFailedNotificationsDTO) => {
|
||||||
|
if (!appCfg.isSmtpConfigured) return;
|
||||||
|
|
||||||
|
await queueService.queue(
|
||||||
|
QueueName.AppConnectionSecretSync,
|
||||||
|
QueueJobs.AppConnectionSendSecretSyncActionFailedNotifications,
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
jobId: `secret-sync-${payload.secretSync.id}-failed-notifications`,
|
||||||
|
attempts: 5,
|
||||||
|
delay: 1000 * 60,
|
||||||
|
backoff: {
|
||||||
|
type: "exponential",
|
||||||
|
delay: 3000
|
||||||
|
},
|
||||||
|
removeOnFail: true,
|
||||||
|
removeOnComplete: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const $syncSecrets = async (job: TSecretSyncDTO) => {
|
||||||
|
const {
|
||||||
|
data: { syncId, auditLogInfo }
|
||||||
|
} = job;
|
||||||
|
|
||||||
|
const secretSync = await secretSyncDAL.findById(syncId);
|
||||||
|
|
||||||
|
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||||
|
|
||||||
|
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;
|
||||||
|
const isFinalAttempt = job.attemptsStarted === job.opts.attempts;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
connection: { orgId, encryptedCredentials }
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
const credentials = await decryptAppConnectionCredentials({
|
||||||
|
orgId,
|
||||||
|
encryptedCredentials,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretMap = await $getSecrets(secretSync);
|
||||||
|
|
||||||
|
await SecretSyncFns.sync(
|
||||||
|
{
|
||||||
|
...secretSync,
|
||||||
|
connection: {
|
||||||
|
...secretSync.connection,
|
||||||
|
credentials
|
||||||
|
}
|
||||||
|
} as TSecretSyncWithConnection,
|
||||||
|
secretMap
|
||||||
|
);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
syncErrorHistogram.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 =
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
(err instanceof AxiosError
|
||||||
|
? err?.response?.data
|
||||||
|
? JSON.stringify(err?.response?.data)
|
||||||
|
: err?.message
|
||||||
|
: (err as Error)?.message) || "An unknown error occurred.";
|
||||||
|
|
||||||
|
// re-throw so job fails
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
const ranAt = new Date();
|
||||||
|
const syncStatus = isSynced ? SecretSyncStatus.Success : SecretSyncStatus.Failed;
|
||||||
|
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
projectId: secretSync.projectId,
|
||||||
|
...(auditLogInfo ?? {
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM,
|
||||||
|
metadata: {}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
event: {
|
||||||
|
type: EventType.SYNC_SECRET_SYNC,
|
||||||
|
metadata: {
|
||||||
|
syncId: secretSync.id,
|
||||||
|
syncOptions: secretSync.syncOptions,
|
||||||
|
environment: secretSync.environment,
|
||||||
|
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.Sync
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("SecretSync Sync Job with ID %s Completed", job.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const $importSecrets = async (job: TSecretSyncImportDTO) => {
|
||||||
|
const {
|
||||||
|
data: { syncId, auditLogInfo, shouldOverwrite }
|
||||||
|
} = job;
|
||||||
|
|
||||||
|
const secretSync = await secretSyncDAL.findById(syncId);
|
||||||
|
|
||||||
|
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`SecretSync Import [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||||
|
);
|
||||||
|
|
||||||
|
let isImported = false;
|
||||||
|
let importMessage: string | null = null;
|
||||||
|
const isFinalAttempt = job.attemptsStarted === job.opts.attempts;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
connection: { orgId, encryptedCredentials },
|
||||||
|
projectId,
|
||||||
|
environment
|
||||||
|
} = secretSync;
|
||||||
|
|
||||||
|
const credentials = await decryptAppConnectionCredentials({
|
||||||
|
orgId,
|
||||||
|
encryptedCredentials,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
const importedSecrets = await SecretSyncFns.import({
|
||||||
|
...secretSync,
|
||||||
|
connection: {
|
||||||
|
...secretSync.connection,
|
||||||
|
credentials
|
||||||
|
}
|
||||||
|
} as TSecretSyncWithConnection);
|
||||||
|
|
||||||
|
if (Object.keys(importedSecrets).length) {
|
||||||
|
const secretMap = await $getSecrets(secretSync, false);
|
||||||
|
|
||||||
|
const secretsToCreate: Parameters<typeof $createManySecretsRawFn>[0]["secrets"] = [];
|
||||||
|
const secretsToUpdate: Parameters<typeof $updateManySecretsRawFn>[0]["secrets"] = [];
|
||||||
|
|
||||||
|
Object.entries(importedSecrets).forEach(([key, { value }]) => {
|
||||||
|
const secret = {
|
||||||
|
secretName: key,
|
||||||
|
secretValue: value,
|
||||||
|
type: SecretType.Shared,
|
||||||
|
secretComment: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Object.hasOwn(secretMap, key)) {
|
||||||
|
secretsToUpdate.push(secret);
|
||||||
|
} else {
|
||||||
|
secretsToCreate.push(secret);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (secretsToCreate.length) {
|
||||||
|
await $createManySecretsRawFn({
|
||||||
|
projectId,
|
||||||
|
path: secretSync.folder.path,
|
||||||
|
environment: environment.slug,
|
||||||
|
secrets: secretsToCreate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldOverwrite && secretsToUpdate.length) {
|
||||||
|
await $updateManySecretsRawFn({
|
||||||
|
projectId,
|
||||||
|
path: secretSync.folder.path,
|
||||||
|
environment: environment.slug,
|
||||||
|
secrets: secretsToUpdate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isImported = 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) {
|
||||||
|
importErrorHistogram.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 =
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
(err instanceof AxiosError
|
||||||
|
? err?.response?.data
|
||||||
|
? JSON.stringify(err?.response?.data)
|
||||||
|
: err?.message
|
||||||
|
: (err as Error)?.message) || "An unknown error occurred.";
|
||||||
|
|
||||||
|
// re-throw so job fails
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
const ranAt = new Date();
|
||||||
|
const importStatus = isImported ? SecretSyncStatus.Success : SecretSyncStatus.Failed;
|
||||||
|
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
projectId: secretSync.projectId,
|
||||||
|
...(auditLogInfo ?? {
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM,
|
||||||
|
metadata: {}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
event: {
|
||||||
|
type: EventType.IMPORT_SECRET_SYNC,
|
||||||
|
metadata: {
|
||||||
|
syncId: secretSync.id,
|
||||||
|
syncOptions: secretSync.syncOptions,
|
||||||
|
environment: secretSync.environment,
|
||||||
|
destination: secretSync.destination,
|
||||||
|
destinationConfig: secretSync.destinationConfig,
|
||||||
|
folderId: secretSync.folderId,
|
||||||
|
connectionId: secretSync.connectionId,
|
||||||
|
jobRanAt: ranAt,
|
||||||
|
jobId: job.id!,
|
||||||
|
importStatus,
|
||||||
|
importMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isImported || isFinalAttempt) {
|
||||||
|
const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
|
||||||
|
importStatus,
|
||||||
|
lastImportJobId: job.id,
|
||||||
|
lastImportMessage: importMessage,
|
||||||
|
lastImportedAt: isImported ? ranAt : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isImported) {
|
||||||
|
await $queueSendSecretSyncFailedNotifications({
|
||||||
|
secretSync: updatedSecretSync,
|
||||||
|
action: SecretSyncAction.Import
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("SecretSync Import Job with ID %s Completed", job.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const $eraseSecrets = async (job: TSecretSyncEraseDTO) => {
|
||||||
|
const {
|
||||||
|
data: { syncId, auditLogInfo }
|
||||||
|
} = job;
|
||||||
|
|
||||||
|
const secretSync = await secretSyncDAL.findById(syncId);
|
||||||
|
|
||||||
|
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`SecretSync Erase [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||||
|
);
|
||||||
|
|
||||||
|
let isErased = false;
|
||||||
|
let eraseMessage: 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 $getSecrets(secretSync);
|
||||||
|
|
||||||
|
await SecretSyncFns.erase(
|
||||||
|
{
|
||||||
|
...secretSync,
|
||||||
|
connection: {
|
||||||
|
...secretSync.connection,
|
||||||
|
credentials
|
||||||
|
}
|
||||||
|
} as TSecretSyncWithConnection,
|
||||||
|
secretMap
|
||||||
|
);
|
||||||
|
|
||||||
|
isErased = true;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
err,
|
||||||
|
`SecretSync Erase Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
eraseErrorHistogram.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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
eraseMessage =
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
(err instanceof AxiosError
|
||||||
|
? err?.response?.data
|
||||||
|
? JSON.stringify(err?.response?.data)
|
||||||
|
: err?.message
|
||||||
|
: (err as Error)?.message) || "An unknown error occurred.";
|
||||||
|
|
||||||
|
// re-throw so job fails
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
const ranAt = new Date();
|
||||||
|
const eraseStatus = isErased ? SecretSyncStatus.Success : SecretSyncStatus.Failed;
|
||||||
|
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
projectId: secretSync.projectId,
|
||||||
|
...(auditLogInfo ?? {
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM,
|
||||||
|
metadata: {}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
event: {
|
||||||
|
type: EventType.ERASE_SECRET_SYNC,
|
||||||
|
metadata: {
|
||||||
|
syncId: secretSync.id,
|
||||||
|
syncOptions: secretSync.syncOptions,
|
||||||
|
environment: secretSync.environment,
|
||||||
|
destination: secretSync.destination,
|
||||||
|
destinationConfig: secretSync.destinationConfig,
|
||||||
|
folderId: secretSync.folderId,
|
||||||
|
connectionId: secretSync.connectionId,
|
||||||
|
jobRanAt: ranAt,
|
||||||
|
jobId: job.id!,
|
||||||
|
eraseStatus,
|
||||||
|
eraseMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isErased || isFinalAttempt) {
|
||||||
|
const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
|
||||||
|
eraseStatus,
|
||||||
|
lastEraseJobId: job.id,
|
||||||
|
lastEraseMessage: eraseMessage,
|
||||||
|
lastErasedAt: isErased ? ranAt : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isErased) {
|
||||||
|
await $queueSendSecretSyncFailedNotifications({
|
||||||
|
secretSync: updatedSecretSync,
|
||||||
|
action: SecretSyncAction.Erase
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("SecretSync Erase Job with ID %s Completed", job.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const $sendSecretSyncFailedNotifications = async (job: TSendSecretSyncFailedNotificationsJobDTO) => {
|
||||||
|
const {
|
||||||
|
data: { secretSync, auditLogInfo, action }
|
||||||
|
} = job;
|
||||||
|
|
||||||
|
const { projectId, destination, name, folder, lastSyncMessage, lastEraseMessage, 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 subject: string;
|
||||||
|
let failureMessage: string | null | undefined;
|
||||||
|
let content: string;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case SecretSyncAction.Import:
|
||||||
|
subject = "Import";
|
||||||
|
failureMessage = lastImportMessage;
|
||||||
|
content = `Your ${syncDestination} Sync named "${name}" failed while attempting to import secrets.`;
|
||||||
|
break;
|
||||||
|
case SecretSyncAction.Erase:
|
||||||
|
subject = "Erase";
|
||||||
|
failureMessage = lastEraseMessage;
|
||||||
|
content = `Your ${syncDestination} Sync named "${name}" failed while attempting to erase secrets.`;
|
||||||
|
break;
|
||||||
|
case SecretSyncAction.Sync:
|
||||||
|
default:
|
||||||
|
subject = `Sync`;
|
||||||
|
failureMessage = lastSyncMessage;
|
||||||
|
content = `Your ${syncDestination} Sync named "${name}" failed to sync.`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||||
|
template: SmtpTemplates.SecretSyncFailed,
|
||||||
|
subjectLine: `Secret Sync Failed to ${subject} Secrets`,
|
||||||
|
substitutions: {
|
||||||
|
syncName: name,
|
||||||
|
syncDestination,
|
||||||
|
content,
|
||||||
|
failureMessage,
|
||||||
|
secretPath: folder.path,
|
||||||
|
environment: environment.name,
|
||||||
|
projectName: project.name,
|
||||||
|
// TODO (scott): verify this is still the URL after bare react change
|
||||||
|
syncUrl: `${appCfg.SITE_URL}/integrations/secret-syncs/${destination}/${secretSync.id}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const queueSecretSyncsByPath = 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, isEnabled: true });
|
||||||
|
|
||||||
|
await Promise.all(secretSyncs.map((secretSync) => queueSecretSyncById({ syncId: secretSync.id })));
|
||||||
|
};
|
||||||
|
|
||||||
|
queueService.start(QueueName.AppConnectionSecretSync, async (job) => {
|
||||||
|
if (job.name === QueueJobs.AppConnectionSendSecretSyncActionFailedNotifications) {
|
||||||
|
await $sendSecretSyncFailedNotifications(job as TSendSecretSyncFailedNotificationsJobDTO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { syncId } = job.data as
|
||||||
|
| TQueueSecretSyncByIdDTO
|
||||||
|
| TQueueSecretSyncImportByIdDTO
|
||||||
|
| TQueueSecretSyncEraseByIdDTO;
|
||||||
|
|
||||||
|
const lock = await keyStore.acquireLock([KeyStorePrefixes.SecretSyncLock(syncId)], 5 * 60 * 1000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (job.name) {
|
||||||
|
case QueueJobs.AppConnectionSecretSync:
|
||||||
|
await $syncSecrets(job as TSecretSyncDTO);
|
||||||
|
break;
|
||||||
|
case QueueJobs.AppConnectionSecretSyncImport:
|
||||||
|
await $importSecrets(job as TSecretSyncImportDTO);
|
||||||
|
break;
|
||||||
|
case QueueJobs.AppConnectionSecretSyncErase:
|
||||||
|
await $eraseSecrets(job as TSecretSyncEraseDTO);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InternalServerError({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
message: `Unhandled Secret Sync Job ${job.name}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await lock.release();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
queueSecretSyncById,
|
||||||
|
queueSecretSyncImportById,
|
||||||
|
queueSecretSyncEraseById,
|
||||||
|
queueSecretSyncsByPath
|
||||||
|
};
|
||||||
|
};
|
65
backend/src/services/secret-sync/secret-sync-schemas.ts
Normal file
65
backend/src/services/secret-sync/secret-sync-schemas.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs";
|
||||||
|
import { SecretSyncs } from "@app/lib/api-docs";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
|
const SyncOptionsSchema = z.object({
|
||||||
|
prependPrefix: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.transform((str) => str.toUpperCase())
|
||||||
|
.optional()
|
||||||
|
.describe(SecretSyncs.SYNC_OPTIONS.PREPEND_PREFIX),
|
||||||
|
appendSuffix: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.transform((str) => str.toUpperCase())
|
||||||
|
.optional()
|
||||||
|
.describe(SecretSyncs.SYNC_OPTIONS.APPEND_SUFFIX)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BaseSecretSyncSchema = (app: AppConnection) =>
|
||||||
|
SecretSyncsSchema.omit({
|
||||||
|
destination: true,
|
||||||
|
destinationConfig: true,
|
||||||
|
syncOptions: true
|
||||||
|
}).extend({
|
||||||
|
syncOptions: SyncOptionsSchema,
|
||||||
|
// join properties
|
||||||
|
projectId: z.string(),
|
||||||
|
connection: z.object({ app: z.literal(app), name: z.string(), id: z.string().uuid() }),
|
||||||
|
environment: z.object({ slug: z.string(), name: z.string(), id: z.string().uuid() }),
|
||||||
|
folder: z.object({ id: z.string(), path: z.string() })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GenericCreateSecretSyncFieldsSchema = (sync: SecretSync) =>
|
||||||
|
z.object({
|
||||||
|
name: slugSchema({ field: "name" }).describe(SecretSyncs.CREATE(sync).name),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description cannot exceed 256 characters")
|
||||||
|
.nullish()
|
||||||
|
.describe(SecretSyncs.CREATE(sync).description),
|
||||||
|
connectionId: z.string().uuid().describe(SecretSyncs.CREATE(sync).connectionId),
|
||||||
|
folderId: z.string().uuid().describe(SecretSyncs.CREATE(sync).folderId),
|
||||||
|
isEnabled: z.boolean().default(true).describe(SecretSyncs.CREATE(sync).isEnabled),
|
||||||
|
syncOptions: SyncOptionsSchema.optional().default({}).describe(SecretSyncs.CREATE(sync).syncOptions)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GenericUpdateSecretSyncFieldsSchema = (sync: SecretSync) =>
|
||||||
|
z.object({
|
||||||
|
name: slugSchema({ field: "name" }).describe(SecretSyncs.UPDATE(sync).name).optional(),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description cannot exceed 256 characters")
|
||||||
|
.nullish()
|
||||||
|
.describe(SecretSyncs.UPDATE(sync).description),
|
||||||
|
folderId: z.string().uuid().optional().describe(SecretSyncs.UPDATE(sync).folderId),
|
||||||
|
isEnabled: z.boolean().optional().describe(SecretSyncs.UPDATE(sync).isEnabled),
|
||||||
|
syncOptions: SyncOptionsSchema.optional().describe(SecretSyncs.UPDATE(sync).syncOptions)
|
||||||
|
});
|
472
backend/src/services/secret-sync/secret-sync-service.ts
Normal file
472
backend/src/services/secret-sync/secret-sync-service.ts
Normal file
@ -0,0 +1,472 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ProjectType } from "@app/db/schemas";
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { startsWithVowel } from "@app/lib/fn";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
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 { listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns";
|
||||||
|
import {
|
||||||
|
TCreateSecretSyncDTO,
|
||||||
|
TDeleteSecretSyncDTO,
|
||||||
|
TFindSecretSyncByIdDTO,
|
||||||
|
TFindSecretSyncByNameDTO,
|
||||||
|
TListSecretSyncsByProjectId,
|
||||||
|
TSecretSync,
|
||||||
|
TTriggerSecretSyncByIdDTO,
|
||||||
|
TTriggerSecretSyncEraseByIdDTO,
|
||||||
|
TTriggerSecretSyncImportByIdDTO,
|
||||||
|
TUpdateSecretSyncDTO
|
||||||
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
import { TSecretSyncDALFactory } from "./secret-sync-dal";
|
||||||
|
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps";
|
||||||
|
import { TSecretSyncQueueFactory } from "./secret-sync-queue";
|
||||||
|
|
||||||
|
type TSecretSyncServiceFactoryDep = {
|
||||||
|
secretSyncDAL: TSecretSyncDALFactory;
|
||||||
|
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById">;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "getItem">;
|
||||||
|
secretSyncQueue: Pick<
|
||||||
|
TSecretSyncQueueFactory,
|
||||||
|
"queueSecretSyncById" | "queueSecretSyncImportById" | "queueSecretSyncEraseById"
|
||||||
|
>;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">; // TODO: remove once launched
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSecretSyncServiceFactory = ReturnType<typeof secretSyncServiceFactory>;
|
||||||
|
|
||||||
|
export const secretSyncServiceFactory = ({
|
||||||
|
secretSyncDAL,
|
||||||
|
folderDAL,
|
||||||
|
licenseService,
|
||||||
|
permissionService,
|
||||||
|
appConnectionService,
|
||||||
|
projectBotService,
|
||||||
|
secretSyncQueue,
|
||||||
|
keyStore
|
||||||
|
}: TSecretSyncServiceFactoryDep) => {
|
||||||
|
// secret syncs are disabled for public until launch
|
||||||
|
const checkSecretSyncAvailability = async (orgId: string) => {
|
||||||
|
const subscription = await licenseService.getPlan(orgId);
|
||||||
|
|
||||||
|
if (!subscription.appConnections) throw new BadRequestError({ message: "Secret Syncs are not available yet." });
|
||||||
|
};
|
||||||
|
|
||||||
|
const listSecretSyncsByProjectId = async (
|
||||||
|
{ projectId, destination }: TListSecretSyncsByProjectId,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
await checkSecretSyncAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
projectId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||||
|
|
||||||
|
const folders = await folderDAL.findByProjectId(projectId);
|
||||||
|
|
||||||
|
const secretSyncs = await secretSyncDAL.find({
|
||||||
|
...(destination && { destination }),
|
||||||
|
$in: {
|
||||||
|
folderId: folders.map((folder) => folder.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return secretSyncs as TSecretSync[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => {
|
||||||
|
await checkSecretSyncAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const secretSync = await secretSyncDAL.findById(syncId);
|
||||||
|
|
||||||
|
if (!secretSync)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
secretSync.projectId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||||
|
|
||||||
|
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
|
||||||
|
});
|
||||||
|
|
||||||
|
return secretSync as TSecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findSecretSyncByName = async (
|
||||||
|
{ destination, syncName, projectId }: TFindSecretSyncByNameDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
await checkSecretSyncAvailability(actor.orgId);
|
||||||
|
|
||||||
|
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({
|
||||||
|
name: syncName,
|
||||||
|
$in: {
|
||||||
|
folderId: folders.map((folder) => folder.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!secretSync)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with name "${syncName}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
secretSync.projectId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||||
|
|
||||||
|
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
|
||||||
|
});
|
||||||
|
|
||||||
|
return secretSync as TSecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSecretSync = async (params: TCreateSecretSyncDTO, actor: OrgServiceActor) => {
|
||||||
|
await checkSecretSyncAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const folder = await folderDAL.findById(params.folderId);
|
||||||
|
|
||||||
|
if (!folder) throw new BadRequestError({ message: `Could not find Folder with ID "${params.folderId}"` });
|
||||||
|
|
||||||
|
const { permission: projectPermission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
folder.projectId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(folder.projectId);
|
||||||
|
|
||||||
|
if (!shouldUseSecretV2Bridge)
|
||||||
|
throw new BadRequestError({ message: "Project version does not support Secret Syncs" });
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||||
|
|
||||||
|
ForbiddenError.from(projectPermission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.SecretSyncs
|
||||||
|
);
|
||||||
|
|
||||||
|
const appConnection = await appConnectionService.connectAppConnectionById(params.connectionId, actor);
|
||||||
|
|
||||||
|
const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination];
|
||||||
|
|
||||||
|
if (appConnection.app !== destinationApp) {
|
||||||
|
const appName = APP_CONNECTION_NAME_MAP[appConnection.app];
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid App Connection - Cannot sync to ${SECRET_SYNC_NAME_MAP[params.destination]} using ${
|
||||||
|
startsWithVowel(appName) ? "an" : "a"
|
||||||
|
} ${appName} Connection`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectFolders = await folderDAL.findByProjectId(folder.projectId);
|
||||||
|
|
||||||
|
const secretSync = await secretSyncDAL.transaction(async (tx) => {
|
||||||
|
const isConflictingName = Boolean(
|
||||||
|
(
|
||||||
|
await secretSyncDAL.find(
|
||||||
|
{
|
||||||
|
name: params.name,
|
||||||
|
$in: {
|
||||||
|
folderId: projectFolders.map((f) => f.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
).length
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isConflictingName)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `A Secret Sync with the name "${params.name}" already exists for the project with ID "${folder.projectId}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
const sync = await secretSyncDAL.create(params);
|
||||||
|
|
||||||
|
return sync;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (secretSync.isEnabled) await secretSyncQueue.queueSecretSyncById({ syncId: secretSync.id });
|
||||||
|
|
||||||
|
return secretSync as TSecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSecretSync = async ({ destination, syncId, ...params }: TUpdateSecretSyncDTO, actor: OrgServiceActor) => {
|
||||||
|
await checkSecretSyncAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const secretSync = await secretSyncDAL.findById(syncId);
|
||||||
|
|
||||||
|
if (!secretSync)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID ${syncId}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
secretSync.projectId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretSyncs);
|
||||||
|
|
||||||
|
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedSecretSync = await secretSyncDAL.transaction(async (tx) => {
|
||||||
|
if (params.folderId) {
|
||||||
|
const newFolder = await folderDAL.findById(params.folderId);
|
||||||
|
|
||||||
|
if (!newFolder) throw new BadRequestError({ message: `Could not find folder with ID "${params.folderId}"` });
|
||||||
|
|
||||||
|
// TODO (scott): I don't think there's a reason we can't allow moving syncs across projects
|
||||||
|
// but not supporting this initially
|
||||||
|
if (newFolder.projectId !== secretSync.projectId)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Cannot move Secret Sync to different project`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.name && secretSync.name !== params.name) {
|
||||||
|
const projectFolders = await folderDAL.findByProjectId(secretSync.projectId);
|
||||||
|
|
||||||
|
const isConflictingName = Boolean(
|
||||||
|
(
|
||||||
|
await secretSyncDAL.find(
|
||||||
|
{
|
||||||
|
name: params.name,
|
||||||
|
$in: {
|
||||||
|
folderId: projectFolders.map((f) => f.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
).length
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isConflictingName)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `A Secret Sync with the name "${params.name}" already exists for project with ID "${secretSync.projectId}"`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSync = await secretSyncDAL.updateById(syncId, params);
|
||||||
|
|
||||||
|
return updatedSync;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updatedSecretSync.isEnabled) await secretSyncQueue.queueSecretSyncById({ syncId: secretSync.id });
|
||||||
|
|
||||||
|
return updatedSecretSync as TSecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSecretSync = async ({ destination, syncId }: TDeleteSecretSyncDTO, actor: OrgServiceActor) => {
|
||||||
|
await checkSecretSyncAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const secretSync = await secretSyncDAL.findById(syncId);
|
||||||
|
|
||||||
|
if (!secretSync)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
secretSync.projectId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretSyncs);
|
||||||
|
|
||||||
|
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
|
||||||
|
});
|
||||||
|
|
||||||
|
await secretSyncDAL.deleteById(syncId);
|
||||||
|
|
||||||
|
return secretSync as TSecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerSecretSyncById = async (
|
||||||
|
{ syncId, destination, ...params }: TTriggerSecretSyncByIdDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
await checkSecretSyncAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const secretSync = await secretSyncDAL.findById(syncId);
|
||||||
|
|
||||||
|
if (!secretSync)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
secretSync.projectId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||||
|
|
||||||
|
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
|
||||||
|
});
|
||||||
|
|
||||||
|
await secretSyncQueue.queueSecretSyncById({ syncId, ...params });
|
||||||
|
|
||||||
|
return secretSync as TSecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerSecretSyncImportById = async (
|
||||||
|
{ syncId, destination, ...params }: TTriggerSecretSyncImportByIdDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
await checkSecretSyncAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const secretSync = await secretSyncDAL.findById(syncId);
|
||||||
|
|
||||||
|
if (!secretSync)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
secretSync.projectId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||||
|
|
||||||
|
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSyncJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretSyncLock(syncId)));
|
||||||
|
|
||||||
|
if (isSyncJobRunning)
|
||||||
|
throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` });
|
||||||
|
|
||||||
|
await secretSyncQueue.queueSecretSyncImportById({ syncId, ...params });
|
||||||
|
|
||||||
|
return secretSync as TSecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerSecretSyncEraseById = async (
|
||||||
|
{ syncId, destination, ...params }: TTriggerSecretSyncEraseByIdDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
await checkSecretSyncAvailability(actor.orgId);
|
||||||
|
|
||||||
|
const secretSync = await secretSyncDAL.findById(syncId);
|
||||||
|
|
||||||
|
if (!secretSync)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
secretSync.projectId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbidOnInvalidProjectType(ProjectType.SecretManager);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||||
|
|
||||||
|
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Secret sync with ID "${secretSync.id}" is not configured for ${SECRET_SYNC_NAME_MAP[destination]}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSyncJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretSyncLock(syncId)));
|
||||||
|
|
||||||
|
if (isSyncJobRunning)
|
||||||
|
throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` });
|
||||||
|
|
||||||
|
await secretSyncQueue.queueSecretSyncEraseById({ syncId, ...params });
|
||||||
|
|
||||||
|
return secretSync as TSecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listSecretSyncOptions,
|
||||||
|
listSecretSyncsByProjectId,
|
||||||
|
findSecretSyncById,
|
||||||
|
findSecretSyncByName,
|
||||||
|
createSecretSync,
|
||||||
|
updateSecretSync,
|
||||||
|
deleteSecretSync,
|
||||||
|
triggerSecretSyncById,
|
||||||
|
triggerSecretSyncImportById,
|
||||||
|
triggerSecretSyncEraseById
|
||||||
|
};
|
||||||
|
};
|
131
backend/src/services/secret-sync/secret-sync-types.ts
Normal file
131
backend/src/services/secret-sync/secret-sync-types.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { Job } from "bullmq";
|
||||||
|
|
||||||
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { QueueJobs } from "@app/queue";
|
||||||
|
import { TSecretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal";
|
||||||
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TAwsParameterStoreSync,
|
||||||
|
TAwsParameterStoreSyncInput,
|
||||||
|
TAwsParameterStoreSyncListItem,
|
||||||
|
TAwsParameterStoreSyncWithConnection
|
||||||
|
} from "./aws-parameter-store";
|
||||||
|
|
||||||
|
export type TSecretSync = TAwsParameterStoreSync;
|
||||||
|
|
||||||
|
export type TSecretSyncWithConnection = TAwsParameterStoreSyncWithConnection;
|
||||||
|
|
||||||
|
export type TSecretSyncInput = TAwsParameterStoreSyncInput;
|
||||||
|
|
||||||
|
export type TSecretSyncListItem = TAwsParameterStoreSyncListItem;
|
||||||
|
|
||||||
|
export type TListSecretSyncsByProjectId = {
|
||||||
|
projectId: string;
|
||||||
|
destination?: SecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TFindSecretSyncByIdDTO = {
|
||||||
|
syncId: string;
|
||||||
|
destination: SecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TFindSecretSyncByNameDTO = {
|
||||||
|
syncName: string;
|
||||||
|
projectId: string;
|
||||||
|
destination: SecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateSecretSyncDTO = Pick<
|
||||||
|
TSecretSync,
|
||||||
|
"syncOptions" | "destinationConfig" | "folderId" | "name" | "connectionId"
|
||||||
|
> & { destination: SecretSync };
|
||||||
|
|
||||||
|
export type TUpdateSecretSyncDTO = Partial<Omit<TCreateSecretSyncDTO, "connectionId">> & {
|
||||||
|
syncId: string;
|
||||||
|
destination: SecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteSecretSyncDTO = {
|
||||||
|
destination: SecretSync;
|
||||||
|
syncId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AuditLogInfo = Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
|
||||||
|
|
||||||
|
export enum SecretSyncStatus {
|
||||||
|
Pending = "pending",
|
||||||
|
Success = "success",
|
||||||
|
Failed = "failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SecretSyncAction {
|
||||||
|
Sync = "sync",
|
||||||
|
Import = "import",
|
||||||
|
Erase = "erase"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TSecretSyncRaw = NonNullable<Awaited<ReturnType<TSecretSyncDALFactory["findById"]>>>;
|
||||||
|
|
||||||
|
export type TQueueSecretSyncsByPathDTO = {
|
||||||
|
secretPath: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TQueueSecretSyncByIdDTO = {
|
||||||
|
syncId: string;
|
||||||
|
auditLogInfo?: AuditLogInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTriggerSecretSyncByIdDTO = {
|
||||||
|
destination: SecretSync;
|
||||||
|
} & TQueueSecretSyncByIdDTO;
|
||||||
|
|
||||||
|
export type TQueueSecretSyncImportByIdDTO = {
|
||||||
|
syncId: string;
|
||||||
|
shouldOverwrite: boolean;
|
||||||
|
auditLogInfo?: AuditLogInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTriggerSecretSyncImportByIdDTO = {
|
||||||
|
destination: SecretSync;
|
||||||
|
} & TQueueSecretSyncImportByIdDTO;
|
||||||
|
|
||||||
|
export type TQueueSecretSyncEraseByIdDTO = {
|
||||||
|
syncId: string;
|
||||||
|
auditLogInfo?: AuditLogInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TTriggerSecretSyncEraseByIdDTO = {
|
||||||
|
destination: SecretSync;
|
||||||
|
} & TQueueSecretSyncEraseByIdDTO;
|
||||||
|
|
||||||
|
export type TQueueSendSecretSyncActionFailedNotificationsDTO = {
|
||||||
|
secretSync: TSecretSyncRaw;
|
||||||
|
auditLogInfo?: AuditLogInfo;
|
||||||
|
action: SecretSyncAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSecretSyncDTO = Job<TQueueSecretSyncByIdDTO, void, QueueJobs.AppConnectionSecretSync>;
|
||||||
|
export type TSecretSyncImportDTO = Job<TQueueSecretSyncImportByIdDTO, void, QueueJobs.AppConnectionSecretSync>;
|
||||||
|
export type TSecretSyncEraseDTO = Job<TQueueSecretSyncEraseByIdDTO, void, QueueJobs.AppConnectionSecretSync>;
|
||||||
|
|
||||||
|
export type TSendSecretSyncFailedNotificationsJobDTO = Job<
|
||||||
|
TQueueSendSecretSyncActionFailedNotificationsDTO,
|
||||||
|
void,
|
||||||
|
QueueJobs.AppConnectionSendSecretSyncActionFailedNotifications
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type TSecretMap = Record<
|
||||||
|
string,
|
||||||
|
{ value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type TSecretSyncGetSecrets = {
|
||||||
|
projectId: string;
|
||||||
|
folderId: string;
|
||||||
|
secretPath: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
includeImports?: boolean;
|
||||||
|
};
|
@ -29,6 +29,7 @@ import { createManySecretsRawFnFactory, updateManySecretsRawFnFactory } from "@a
|
|||||||
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
||||||
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
|
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
|
||||||
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
|
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
|
||||||
|
import { TSecretSyncQueueFactory } from "@app/services/secret-sync/secret-sync-queue";
|
||||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
@ -104,6 +105,7 @@ type TSecretQueueFactoryDep = {
|
|||||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||||
|
secretSyncQueue: Pick<TSecretSyncQueueFactory, "queueSecretSyncsByPath">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecrets = {
|
export type TGetSecrets = {
|
||||||
@ -157,7 +159,8 @@ export const secretQueueFactory = ({
|
|||||||
auditLogService,
|
auditLogService,
|
||||||
orgService,
|
orgService,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
projectKeyDAL
|
projectKeyDAL,
|
||||||
|
secretSyncQueue
|
||||||
}: TSecretQueueFactoryDep) => {
|
}: TSecretQueueFactoryDep) => {
|
||||||
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
|
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
|
||||||
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
|
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
|
||||||
@ -619,6 +622,9 @@ export const secretQueueFactory = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await secretSyncQueue.queueSecretSyncsByPath({ projectId, environmentSlug: environment, secretPath });
|
||||||
|
|
||||||
await syncIntegrations({ secretPath, projectId, environment, deDupeQueue, isManual: false });
|
await syncIntegrations({ secretPath, projectId, environment, deDupeQueue, isManual: false });
|
||||||
if (!excludeReplication) {
|
if (!excludeReplication) {
|
||||||
await replicateSecrets({
|
await replicateSecrets({
|
||||||
|
@ -35,6 +35,7 @@ export enum SmtpTemplates {
|
|||||||
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
||||||
PkiExpirationAlert = "pkiExpirationAlert.handlebars",
|
PkiExpirationAlert = "pkiExpirationAlert.handlebars",
|
||||||
IntegrationSyncFailed = "integrationSyncFailed.handlebars",
|
IntegrationSyncFailed = "integrationSyncFailed.handlebars",
|
||||||
|
SecretSyncFailed = "secretSyncFailed.handlebars",
|
||||||
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
|
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
|
||||||
ExternalImportFailed = "externalImportFailed.handlebars",
|
ExternalImportFailed = "externalImportFailed.handlebars",
|
||||||
ExternalImportStarted = "externalImportStarted.handlebars"
|
ExternalImportStarted = "externalImportStarted.handlebars"
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<title>{{syncDestination}} Sync "{{syncName}}" Failed</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Infisical</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>{{content}}</p>
|
||||||
|
<a href="{{syncUrl}}">
|
||||||
|
View in Infisical.
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<div>
|
||||||
|
<p><strong>Name</strong>: {{syncName}}</p>
|
||||||
|
<p><strong>Destination</strong>: {{syncDestination}}</p>
|
||||||
|
<p><strong>Project</strong>: {{projectName}}</p>
|
||||||
|
<p><strong>Environment</strong>: {{environment}}</p>
|
||||||
|
<p><strong>Secret Path</strong>: {{secretPath}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if failureMessage}}
|
||||||
|
<p><b>Reason: </b>{{failureMessage}}</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{emailFooter}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Available"
|
||||||
|
openapi: "GET /api/v1/app-connections/aws/available"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Available"
|
||||||
|
openapi: "GET /api/v1/app-connections/github/available"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/aws-parameter-store"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/secret-syncs/aws-parameter-store/{syncId}"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Erase Secrets"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/aws-parameter-store/{syncId}/erase"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v1/secret-syncs/aws-parameter-store/{syncId}"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Name"
|
||||||
|
openapi: "GET /api/v1/secret-syncs/aws-parameter-store/name/{syncName}"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Import Secrets"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/aws-parameter-store/{syncId}/import"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/secret-syncs/aws-parameter-store"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Sync Secrets"
|
||||||
|
openapi: "POST /api/v1/secret-syncs/aws-parameter-store/{syncId}/sync"
|
||||||
|
---
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v1/secret-syncs/aws-parameter-store/{syncId}"
|
||||||
|
---
|
4
docs/api-reference/endpoints/secret-syncs/list.mdx
Normal file
4
docs/api-reference/endpoints/secret-syncs/list.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/secret-syncs"
|
||||||
|
---
|
4
docs/api-reference/endpoints/secret-syncs/options.mdx
Normal file
4
docs/api-reference/endpoints/secret-syncs/options.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Options"
|
||||||
|
openapi: "GET /api/v1/secret-syncs/options"
|
||||||
|
---
|
@ -759,6 +759,55 @@
|
|||||||
"api-reference/endpoints/identity-specific-privilege/list"
|
"api-reference/endpoints/identity-specific-privilege/list"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "App Connections",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/app-connections/list",
|
||||||
|
"api-reference/endpoints/app-connections/options",
|
||||||
|
{ "group": "AWS",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/app-connections/aws/list",
|
||||||
|
"api-reference/endpoints/app-connections/aws/available",
|
||||||
|
"api-reference/endpoints/app-connections/aws/get-by-id",
|
||||||
|
"api-reference/endpoints/app-connections/aws/get-by-name",
|
||||||
|
"api-reference/endpoints/app-connections/aws/create",
|
||||||
|
"api-reference/endpoints/app-connections/aws/update",
|
||||||
|
"api-reference/endpoints/app-connections/aws/delete"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "group": "GitHub",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/app-connections/github/list",
|
||||||
|
"api-reference/endpoints/app-connections/github/available",
|
||||||
|
"api-reference/endpoints/app-connections/github/get-by-id",
|
||||||
|
"api-reference/endpoints/app-connections/github/get-by-name",
|
||||||
|
"api-reference/endpoints/app-connections/github/create",
|
||||||
|
"api-reference/endpoints/app-connections/github/update",
|
||||||
|
"api-reference/endpoints/app-connections/github/delete"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Secret Syncs",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/secret-syncs/list",
|
||||||
|
"api-reference/endpoints/secret-syncs/options",
|
||||||
|
{ "group": "AWS Parameter Store",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/secret-syncs/aws-parameter-store/list",
|
||||||
|
"api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-id",
|
||||||
|
"api-reference/endpoints/secret-syncs/aws-parameter-store/get-by-name",
|
||||||
|
"api-reference/endpoints/secret-syncs/aws-parameter-store/create",
|
||||||
|
"api-reference/endpoints/secret-syncs/aws-parameter-store/update",
|
||||||
|
"api-reference/endpoints/secret-syncs/aws-parameter-store/delete",
|
||||||
|
"api-reference/endpoints/secret-syncs/aws-parameter-store/sync",
|
||||||
|
"api-reference/endpoints/secret-syncs/aws-parameter-store/import",
|
||||||
|
"api-reference/endpoints/secret-syncs/aws-parameter-store/erase"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Integrations",
|
"group": "Integrations",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
Reference in New Issue
Block a user