diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index 8feee1830..20e46aa69 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -31,7 +31,7 @@ import { type TDynamicSecretLeaseServiceFactoryDep = { dynamicSecretLeaseDAL: TDynamicSecretLeaseDALFactory; - dynamicSecretDAL: Pick; + dynamicSecretDAL: Pick; dynamicSecretProviders: Record; dynamicSecretQueueService: TDynamicSecretLeaseQueueServiceFactory; licenseService: Pick; @@ -78,10 +78,6 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); const plan = await licenseService.getPlan(actorOrgId); if (!plan?.dynamicSecret) { @@ -96,12 +92,21 @@ export const dynamicSecretLeaseServiceFactory = ({ message: `Folder with path '${path}' in environment with slug '${environmentSlug}' not found` }); - const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); + const dynamicSecretCfg = await dynamicSecretDAL.findOneWithMetadata({ name, folderId: folder.id }); if (!dynamicSecretCfg) throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder with path '${path}' not found` }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id); if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT) throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` }); @@ -159,10 +164,6 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, @@ -187,7 +188,20 @@ export const dynamicSecretLeaseServiceFactory = ({ throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` }); } - const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; + const dynamicSecretCfg = await dynamicSecretDAL.findOneWithMetadata({ + id: dynamicSecretLease.dynamicSecretId, + folderId: folder.id + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() @@ -239,10 +253,6 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, @@ -259,7 +269,20 @@ export const dynamicSecretLeaseServiceFactory = ({ if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id) throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` }); - const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; + const dynamicSecretCfg = await dynamicSecretDAL.findOneWithMetadata({ + id: dynamicSecretLease.dynamicSecretId, + folderId: folder.id + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const decryptedStoredInput = JSON.parse( secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString() @@ -309,10 +332,6 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) @@ -320,12 +339,21 @@ export const dynamicSecretLeaseServiceFactory = ({ message: `Folder with path '${path}' in environment with slug '${environmentSlug}' not found` }); - const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); + const dynamicSecretCfg = await dynamicSecretDAL.findOneWithMetadata({ name, folderId: folder.id }); if (!dynamicSecretCfg) throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder with path '${path}' not found` }); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id }); return dynamicSecretLeases; }; @@ -352,10 +380,6 @@ export const dynamicSecretLeaseServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' not found` }); @@ -364,6 +388,20 @@ export const dynamicSecretLeaseServiceFactory = ({ if (!dynamicSecretLease) throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` }); + const dynamicSecretCfg = await dynamicSecretDAL.findOneWithMetadata({ + id: dynamicSecretLease.dynamicSecretId, + folderId: folder.id + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.Lease, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + return dynamicSecretLease; }; diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts index 9efac9fcc..d779103ef 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-dal.ts @@ -1,9 +1,17 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; +import { TableName, TDynamicSecrets } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { + buildFindFilter, + ormify, + prependTableNameToFindFilter, + selectAllTableCols, + sqlNestRelationships, + TFindFilter, + TFindOpt +} from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -12,7 +20,7 @@ export type TDynamicSecretDALFactory = ReturnType { const orm = ormify(db, TableName.DynamicSecret); - const findOneWithMetadata = async (filter: { name?: string; folderId?: string }, tx?: Knex) => { + const findOneWithMetadata = async (filter: TFindFilter, tx?: Knex) => { const query = (tx || db.replicaNode())(TableName.DynamicSecret) .leftJoin( TableName.ResourceMetadata, @@ -25,7 +33,7 @@ export const dynamicSecretDALFactory = (db: TDbClient) => { db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") ) - .where(filter); + .where(prependTableNameToFindFilter(TableName.DynamicSecret, filter)); const docs = sqlNestRelationships({ data: await query, @@ -47,6 +55,51 @@ export const dynamicSecretDALFactory = (db: TDbClient) => { return docs[0]; }; + const findWithMetadata = async ( + filter: TFindFilter, + { offset, limit, sort, tx }: TFindOpt = {} + ) => { + const query = (tx || db.replicaNode())(TableName.DynamicSecret) + .leftJoin( + TableName.ResourceMetadata, + `${TableName.ResourceMetadata}.dynamicSecretId`, + `${TableName.DynamicSecret}.id` + ) + .select(selectAllTableCols(TableName.DynamicSecret)) + .select( + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + ) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .where(buildFindFilter(filter)); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = sqlNestRelationships({ + data: await query, + key: "id", + parentMapper: (el) => el, + childrenMapper: [ + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); + + return docs; + }; + // find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination) const listDynamicSecretsByFolderIds = async ( { @@ -74,18 +127,27 @@ export const dynamicSecretDALFactory = (db: TDbClient) => { void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`); } }) + .leftJoin( + TableName.ResourceMetadata, + `${TableName.ResourceMetadata}.dynamicSecretId`, + `${TableName.DynamicSecret}.id` + ) .leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`) .leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .select( selectAllTableCols(TableName.DynamicSecret), db.ref("slug").withSchema(TableName.Environment).as("environment"), - db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`) + db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.DynamicSecret}."name" ${orderDirection}) as rank`), + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") ) .orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection); + let queryWithLimit; if (limit) { const rankOffset = offset + 1; - return await (tx || db) + queryWithLimit = (tx || db.replicaNode()) .with("w", query) .select("*") .from[number]>("w") @@ -93,7 +155,22 @@ export const dynamicSecretDALFactory = (db: TDbClient) => { .andWhere("w.rank", "<", rankOffset + limit); } - const dynamicSecrets = await query; + const dynamicSecrets = sqlNestRelationships({ + data: await (queryWithLimit || query), + key: "id", + parentMapper: (el) => el, + childrenMapper: [ + { + key: "metadataId", + label: "metadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) + } + ] + }); return dynamicSecrets; } catch (error) { @@ -101,5 +178,5 @@ export const dynamicSecretDALFactory = (db: TDbClient) => { } }; - return { ...orm, listDynamicSecretsByFolderIds, findOneWithMetadata }; + return { ...orm, listDynamicSecretsByFolderIds, findOneWithMetadata, findWithMetadata }; }; diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index b3e93805c..1d6d8e797 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -91,9 +91,10 @@ export const dynamicSecretServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); + ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.CreateRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path, metadata }) ); const plan = await licenseService.getPlan(actorOrgId); @@ -196,10 +197,6 @@ export const dynamicSecretServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.EditRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); const plan = await licenseService.getPlan(actorOrgId); if (!plan?.dynamicSecret) { @@ -212,12 +209,22 @@ export const dynamicSecretServiceFactory = ({ if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` }); - const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); + const dynamicSecretCfg = await dynamicSecretDAL.findOneWithMetadata({ name, folderId: folder.id }); if (!dynamicSecretCfg) { throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` }); } + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.EditRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + if (newName) { const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id }); if (existingDynamicSecret) @@ -315,20 +322,25 @@ export const dynamicSecretServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.DeleteRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` }); - const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); + const dynamicSecretCfg = await dynamicSecretDAL.findOneWithMetadata({ name, folderId: folder.id }); if (!dynamicSecretCfg) { throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` }); } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.DeleteRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id }); // when not forced we check with the external system to first remove the things // we introduce a forced concept because consider the external lease got deleted by some other external like a human or another system @@ -376,14 +388,6 @@ export const dynamicSecretServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.EditRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) @@ -394,6 +398,24 @@ export const dynamicSecretServiceFactory = ({ throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` }); } + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.ReadRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionDynamicSecretActions.EditRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecretCfg.metadata + }) + ); + const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.SecretManager, projectId @@ -475,7 +497,7 @@ export const dynamicSecretServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) + ProjectPermissionSub.DynamicSecrets ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); @@ -522,16 +544,12 @@ export const dynamicSecretServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.SecretManager }); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ); const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path); if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` }); - const dynamicSecretCfg = await dynamicSecretDAL.find( + const dynamicSecretCfg = await dynamicSecretDAL.findWithMetadata( { folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined }, { limit, @@ -539,7 +557,17 @@ export const dynamicSecretServiceFactory = ({ sort: orderBy ? [[orderBy, orderDirection]] : undefined } ); - return dynamicSecretCfg; + + return dynamicSecretCfg.filter((dynamicSecret) => { + return permission.can( + ProjectPermissionDynamicSecretActions.ReadRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: environmentSlug, + secretPath: path, + metadata: dynamicSecret.metadata + }) + ); + }); }; const listDynamicSecretsByFolderIds = async ( @@ -591,24 +619,14 @@ export const dynamicSecretServiceFactory = ({ isInternal, ...params }: TListDynamicSecretsMultiEnvDTO) => { - if (!isInternal) { - const { permission } = await permissionService.getProjectPermission({ - actor, - actorId, - projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.SecretManager - }); - - // verify user has access to each env in request - environmentSlugs.forEach((environmentSlug) => - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path }) - ) - ); - } + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path); if (!folders.length) @@ -621,7 +639,16 @@ export const dynamicSecretServiceFactory = ({ ...params }); - return dynamicSecretCfg; + return dynamicSecretCfg.filter((dynamicSecret) => { + return permission.can( + ProjectPermissionDynamicSecretActions.ReadRootCredential, + subject(ProjectPermissionSub.DynamicSecrets, { + environment: dynamicSecret.environment, + secretPath: path, + metadata: dynamicSecret.metadata + }) + ); + }); }; const fetchAzureEntraIdUsers = async ({ diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index dbb74de65..0c808de32 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -144,6 +144,10 @@ export type SecretFolderSubjectFields = { export type DynamicSecretSubjectFields = { environment: string; secretPath: string; + metadata?: { + key: string; + value: string; + }[]; }; export type SecretImportSubjectFields = { diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index bcb0a949a..23aadf6cd 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -1,13 +1,9 @@ -import { ForbiddenError, subject } from "@casl/ability"; +import { ForbiddenError } from "@casl/ability"; import { z } from "zod"; -import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas"; +import { SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; -import { - ProjectPermissionDynamicSecretActions, - ProjectPermissionSecretActions, - ProjectPermissionSub -} from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; import { DASHBOARD } from "@app/lib/api-docs"; import { BadRequestError } from "@app/lib/errors"; @@ -289,24 +285,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { totalCount: totalFolderCount ?? 0 }; - const { permission } = await server.services.permission.getProjectPermission({ - actor: req.permission.type, - actorId: req.permission.id, - projectId, - actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId, - actionProjectType: ActionProjectType.SecretManager - }); - - const allowedDynamicSecretEnvironments = // filter envs user has access to - environments.filter((environment) => - permission.can( - ProjectPermissionDynamicSecretActions.Lease, - subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath }) - ) - ); - - if (includeDynamicSecrets && allowedDynamicSecretEnvironments.length) { + if (includeDynamicSecrets) { // this is the unique count, ie duplicate secrets across envs only count as 1 totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({ actor: req.permission.type, @@ -315,7 +294,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId, projectId, search, - environmentSlugs: allowedDynamicSecretEnvironments, + environmentSlugs: environments, path: secretPath, isInternal: true }); @@ -330,7 +309,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { search, orderBy, orderDirection, - environmentSlugs: allowedDynamicSecretEnvironments, + environmentSlugs: environments, path: secretPath, limit: remainingLimit, offset: adjustedOffset, diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 0eba72143..c04379fd7 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -185,6 +185,7 @@ export type SecretFolderSubjectFields = { export type DynamicSecretSubjectFields = { environment: string; secretPath: string; + metadata: (string | { key: string; value: string })[]; }; export type SecretImportSubjectFields = { diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 4ee50ed4f..7c35c836f 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -207,7 +207,8 @@ export const OverviewPage = () => { ProjectPermissionDynamicSecretActions.CreateRootCredential, subject(ProjectPermissionSub.DynamicSecrets, { environment: env.slug, - secretPath + secretPath, + metadata: ["*"] }) ) ); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index cd2a1aee1..cc53a0531 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -138,7 +138,7 @@ const Page = () => { const canReadDynamicSecret = permission.can( ProjectPermissionDynamicSecretActions.ReadRootCredential, - subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath }) + subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath, metadata: ["*"] }) ); const canReadSecretRotations = permission.can( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index 7b1965878..7c321df94 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -854,7 +854,8 @@ export const ActionBar = ({ environment, secretPath, secretName: "*", - secretTags: ["*"] + secretTags: ["*"], + metadata: ["*"] })} > {(isAllowed) => ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretLease.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretLease.tsx index b570ba14b..78392ea14 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretLease.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretLease.tsx @@ -29,11 +29,13 @@ import { import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useGetDynamicSecretLeases, useRevokeDynamicSecretLease } from "@app/hooks/api"; +import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; import { DynamicSecretLeaseStatus } from "@app/hooks/api/dynamicSecretLease/types"; import { RenewDynamicSecretLease } from "./RenewDynamicSecretLease"; type Props = { + dynamicSecret: TDynamicSecret; dynamicSecretName: string; projectSlug: string; environment: string; @@ -48,7 +50,8 @@ export const DynamicSecretLease = ({ environment, secretPath, onClickNewLease, - onClose + onClose, + dynamicSecret }: Props) => { const { handlePopUpOpen, popUp, handlePopUpClose, handlePopUpToggle } = usePopUp([ "deleteSecret", @@ -140,7 +143,11 @@ export const DynamicSecretLease = ({
@@ -159,7 +166,11 @@ export const DynamicSecretLease = ({ @@ -181,7 +192,8 @@ export const DynamicSecretLease = ({ I={ProjectPermissionDynamicSecretActions.Lease} a={subject(ProjectPermissionSub.DynamicSecrets, { environment, - secretPath + secretPath, + metadata: dynamicSecret.metadata })} renderTooltip allowedLabel="Force Delete. This action will remove the secret from internal storage, but it will remain in external systems." @@ -215,7 +227,8 @@ export const DynamicSecretLease = ({ I={ProjectPermissionDynamicSecretActions.Lease} a={subject(ProjectPermissionSub.DynamicSecrets, { environment, - secretPath + secretPath, + metadata: dynamicSecret.metadata })} > {(isAllowed) => ( diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretListView.tsx index bfb341a40..d652d49ad 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/DynamicSecretListView.tsx @@ -144,7 +144,11 @@ export const DynamicSecretListView = ({
@@ -186,7 +190,11 @@ export const DynamicSecretListView = ({
@@ -208,7 +216,11 @@ export const DynamicSecretListView = ({ @@ -236,6 +248,7 @@ export const DynamicSecretListView = ({ className="max-w-3xl" > handlePopUpOpen("createDynamicSecretLease", secret)} onClose={() => handlePopUpClose("dynamicSecretLeases")} projectSlug={projectSlug}