diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 0b59aa8a9..136462cca 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -21,6 +21,8 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service"; import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; +import { TDynamicSecretServiceFactory } from "@app/services/dynamic-secret/dynamic-secret-service"; +import { TDynamicSecretLeaseServiceFactory } from "@app/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; @@ -117,6 +119,8 @@ declare module "fastify" { trustedIp: TTrustedIpServiceFactory; secretBlindIndex: TSecretBlindIndexServiceFactory; telemetry: TTelemetryServiceFactory; + dynamicSecret: TDynamicSecretServiceFactory; + dynamicSecretLease: TDynamicSecretLeaseServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index c452bfbd3..90fbd875b 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -47,6 +47,12 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service"; import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal"; import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service"; +import { dynamicSecretDALFactory } from "@app/services/dynamic-secret/dynamic-secret-dal"; +import { dynamicSecretServiceFactory } from "@app/services/dynamic-secret/dynamic-secret-service"; +import { buildDynamicSecretProviders } from "@app/services/dynamic-secret/providers"; +import { dynamicSecretLeaseDALFactory } from "@app/services/dynamic-secret-lease/dynamic-secret-lease-dal"; +import { dynamicSecretLeaseQueueServiceFactory } from "@app/services/dynamic-secret-lease/dynamic-secret-lease-queue"; +import { dynamicSecretLeaseServiceFactory } from "@app/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { identityDALFactory } from "@app/services/identity/identity-dal"; import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal"; import { identityServiceFactory } from "@app/services/identity/identity-service"; @@ -196,6 +202,8 @@ export const registerRoutes = async ( const gitAppOrgDAL = gitAppDALFactory(db); const secretScanningDAL = secretScanningDALFactory(db); const licenseDAL = licenseDALFactory(db); + const dynamicSecretDAL = dynamicSecretDALFactory(db); + const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db); const permissionService = permissionServiceFactory({ permissionDAL, @@ -550,6 +558,30 @@ export const registerRoutes = async ( licenseService }); + const dynamicSecretProviders = buildDynamicSecretProviders(); + const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({ + queueService, + dynamicSecretLeaseDAL, + dynamicSecretProviders, + dynamicSecretDAL + }); + const dynamicSecretService = dynamicSecretServiceFactory({ + dynamicSecretQueueService, + dynamicSecretDAL, + dynamicSecretLeaseDAL, + dynamicSecretProviders, + folderDAL, + permissionService + }); + const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ + permissionService, + dynamicSecretQueueService, + dynamicSecretDAL, + dynamicSecretLeaseDAL, + dynamicSecretProviders, + folderDAL + }); + await superAdminService.initServerCfg(); // // setup the communication with license key server @@ -591,6 +623,8 @@ export const registerRoutes = async ( secretApprovalPolicy: sapService, secretApprovalRequest: sarService, secretRotation: secretRotationService, + dynamicSecret: dynamicSecretService, + dynamicSecretLease: dynamicSecretLeaseService, snapshot: snapshotService, saml: samlService, ldap: ldapService, diff --git a/backend/src/server/routes/v1/dynamic-secret-lease-router.ts b/backend/src/server/routes/v1/dynamic-secret-lease-router.ts new file mode 100644 index 000000000..cf39bdd78 --- /dev/null +++ b/backend/src/server/routes/v1/dynamic-secret-lease-router.ts @@ -0,0 +1,138 @@ +import ms from "ms"; +import { z } from "zod"; + +import { DynamicSecretLeasesSchema } from "@app/db/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + schema: { + body: z.object({ + slug: z.string(), + projectId: z.string(), + ttl: z + .string() + .optional() + .refine((val) => typeof val === "undefined" || ms(val) > 0, "TTL must be a positive number"), + path: z.string().default("/"), + environment: z.string() + }), + response: { + 200: z.object({ + lease: DynamicSecretLeasesSchema, + data: z.unknown() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { data, lease } = await server.services.dynamicSecretLease.create({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + return { lease, data }; + } + }); + + server.route({ + url: "/:leaseId", + method: "DELETE", + schema: { + params: z.object({ + leaseId: z.string() + }), + body: z.object({ + projectId: z.string(), + path: z.string(), + environment: z.string() + }), + response: { + 200: z.object({ + lease: DynamicSecretLeasesSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const lease = await server.services.dynamicSecretLease.revokeLease({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + leaseId: req.params.leaseId, + ...req.body + }); + return { lease }; + } + }); + + server.route({ + url: "/:leaseId/renew", + method: "POST", + schema: { + params: z.object({ + leaseId: z.string() + }), + body: z.object({ + projectId: z.string(), + path: z.string(), + environment: z.string() + }), + response: { + 200: z.object({ + lease: DynamicSecretLeasesSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const lease = await server.services.dynamicSecretLease.renewLease({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + leaseId: req.params.leaseId, + ...req.body + }); + return { lease }; + } + }); + + server.route({ + url: "/:slug", + method: "GET", + schema: { + params: z.object({ + slug: z.string() + }), + querystring: z.object({ + projectId: z.string(), + path: z.string(), + environment: z.string() + }), + response: { + 200: z.object({ + leases: DynamicSecretLeasesSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const leases = await server.services.dynamicSecretLease.listLeases({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + slug: req.params.slug, + ...req.query + }); + return { leases }; + } + }); +}; diff --git a/backend/src/server/routes/v1/dynamic-secret-router.ts b/backend/src/server/routes/v1/dynamic-secret-router.ts new file mode 100644 index 000000000..9c56f8543 --- /dev/null +++ b/backend/src/server/routes/v1/dynamic-secret-router.ts @@ -0,0 +1,146 @@ +import ms from "ms"; +import { z } from "zod"; + +import { DynamicSecretsSchema } from "@app/db/schemas"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { DynamicSecretProviderSchema } from "@app/services/dynamic-secret/providers/models"; + +export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + schema: { + body: z.object({ + projectId: z.string(), + provider: DynamicSecretProviderSchema, + defaultTTL: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"), + maxTTL: z + .string() + .optional() + .refine((val) => typeof val === "undefined" || ms(val) > 0, "Max TTL must be a positive number"), + path: z.string().default("/"), + environment: z.string(), + slug: z.string().toLowerCase() + }), + response: { + 200: z.object({ + dynamicSecret: DynamicSecretsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const dynamicSecretCfg = await server.services.dynamicSecret.create({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + return { dynamicSecret: dynamicSecretCfg }; + } + }); + + server.route({ + url: "/:slug", + method: "PATCH", + schema: { + params: z.object({ + slug: z.string() + }), + body: z.object({ + projectId: z.string(), + inputs: z.any().optional(), + defaultTTL: z + .string() + .optional() + .refine((val) => typeof val === "undefined" || ms(val) > 0, "TTL must be a positive number"), + maxTTL: z + .string() + .optional() + .refine((val) => typeof val === "undefined" || ms(val) > 0, "Max TTL must be a positive number"), + path: z.string(), + environment: z.string(), + newSlug: z.string() + }), + response: { + 200: z.object({ + dynamicSecret: DynamicSecretsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const dynamicSecretCfg = await server.services.dynamicSecret.updateBySlug({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + slug: req.params.slug, + ...req.body + }); + return { dynamicSecret: dynamicSecretCfg }; + } + }); + + server.route({ + url: "/:slug", + method: "DELETE", + schema: { + params: z.object({ + slug: z.string() + }), + body: z.object({ + projectId: z.string(), + path: z.string(), + environment: z.string() + }), + response: { + 200: z.object({ + dynamicSecret: DynamicSecretsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const dynamicSecretCfg = await server.services.dynamicSecret.deleteBySlug({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + slug: req.params.slug, + ...req.body + }); + return { dynamicSecret: dynamicSecretCfg }; + } + }); + + server.route({ + url: "/", + method: "GET", + schema: { + querystring: z.object({ + projectId: z.string(), + path: z.string(), + environment: z.string() + }), + response: { + 200: z.object({ + dynamicSecrets: DynamicSecretsSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const dynamicSecretCfgs = await server.services.dynamicSecret.list({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + return { dynamicSecrets: dynamicSecretCfgs }; + } + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index fbc68d974..917514b72 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -1,6 +1,8 @@ import { registerAdminRouter } from "./admin-router"; import { registerAuthRoutes } from "./auth-router"; import { registerProjectBotRouter } from "./bot-router"; +import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; +import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; import { registerIdentityRouter } from "./identity-router"; import { registerIdentityUaRouter } from "./identity-ua"; @@ -52,6 +54,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { { prefix: "/workspace" } ); + await server.register( + async (dynamicSecretRouter) => { + await dynamicSecretRouter.register(registerDynamicSecretRouter); + await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" }); + }, + { prefix: "/dynamic-secrets" } + ); + await server.register(registerProjectBotRouter, { prefix: "/bot" }); await server.register(registerIntegrationRouter, { prefix: "/integration" }); await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });