mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 16:40:30 +00:00
Merge branch 'main' into feat/addProjectDeletionProtection
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.ResourceMetadata, "dynamicSecretId"))) {
|
||||
await knex.schema.alterTable(TableName.ResourceMetadata, (tb) => {
|
||||
tb.uuid("dynamicSecretId");
|
||||
tb.foreign("dynamicSecretId").references("id").inTable(TableName.DynamicSecret).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.ResourceMetadata, "dynamicSecretId")) {
|
||||
await knex.schema.alterTable(TableName.ResourceMetadata, (tb) => {
|
||||
tb.dropColumn("dynamicSecretId");
|
||||
});
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.string("altNames", 4096).alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.string("altNames").alter(); // Defaults to varchar(255)
|
||||
});
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.KmipOrgServerCertificates, (t) => {
|
||||
t.string("altNames", 4096).alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(TableName.KmipOrgServerCertificates, (t) => {
|
||||
t.string("altNames").alter(); // Defaults to varchar(255)
|
||||
});
|
||||
}
|
||||
@@ -16,7 +16,8 @@ export const ResourceMetadataSchema = z.object({
|
||||
identityId: z.string().uuid().nullable().optional(),
|
||||
secretId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
dynamicSecretId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TResourceMetadata = z.infer<typeof ResourceMetadataSchema>;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
|
||||
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -48,7 +49,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
.nullable(),
|
||||
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
||||
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
|
||||
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name)
|
||||
name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name),
|
||||
metadata: ResourceMetadataSchema.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -143,7 +145,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional()
|
||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
|
||||
metadata: ResourceMetadataSchema.optional()
|
||||
})
|
||||
}),
|
||||
response: {
|
||||
@@ -238,6 +241,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
name: req.params.name,
|
||||
...req.query
|
||||
});
|
||||
|
||||
return { dynamicSecret: dynamicSecretCfg };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
@@ -102,6 +98,15 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
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,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||
}
|
||||
|
||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({
|
||||
id: dynamicSecretLease.dynamicSecretId,
|
||||
folderId: folder.id
|
||||
});
|
||||
|
||||
if (!dynamicSecretCfg)
|
||||
throw new NotFoundError({
|
||||
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
|
||||
});
|
||||
|
||||
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 +258,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 +274,25 @@ 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.findOne({
|
||||
id: dynamicSecretLease.dynamicSecretId,
|
||||
folderId: folder.id
|
||||
});
|
||||
|
||||
if (!dynamicSecretCfg)
|
||||
throw new NotFoundError({
|
||||
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
|
||||
});
|
||||
|
||||
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 +342,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)
|
||||
@@ -326,6 +355,15 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
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 +390,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 +398,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
if (!dynamicSecretLease)
|
||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({
|
||||
id: dynamicSecretLease.dynamicSecretId,
|
||||
folderId: folder.id
|
||||
});
|
||||
|
||||
if (!dynamicSecretCfg)
|
||||
throw new NotFoundError({
|
||||
message: `Dynamic secret with ID '${dynamicSecretLease.dynamicSecretId}' not found`
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.Lease,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: environmentSlug,
|
||||
secretPath: path,
|
||||
metadata: dynamicSecretCfg.metadata
|
||||
})
|
||||
);
|
||||
|
||||
return dynamicSecretLease;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 } 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,6 +20,86 @@ export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory
|
||||
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.DynamicSecret);
|
||||
|
||||
const findOne = async (filter: TFindFilter<TDynamicSecrets>, tx?: Knex) => {
|
||||
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")
|
||||
)
|
||||
.where(prependTableNameToFindFilter(TableName.DynamicSecret, filter));
|
||||
|
||||
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[0];
|
||||
};
|
||||
|
||||
const findWithMetadata = async (
|
||||
filter: TFindFilter<TDynamicSecrets>,
|
||||
{ offset, limit, sort, tx }: TFindOpt<TDynamicSecrets> = {}
|
||||
) => {
|
||||
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 (
|
||||
{
|
||||
@@ -39,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<Awaited<typeof query>[number]>("w")
|
||||
@@ -58,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) {
|
||||
@@ -66,5 +178,5 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...orm, listDynamicSecretsByFolderIds };
|
||||
return { ...orm, listDynamicSecretsByFolderIds, findOne, findWithMetadata };
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ export const verifyHostInputValidity = async (host: string, isGateway = false) =
|
||||
inputHostIps.push(...resolvedIps);
|
||||
}
|
||||
|
||||
if (!isGateway && !appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP) {
|
||||
if (!isGateway && !(appCfg.DYNAMIC_SECRET_ALLOW_INTERNAL_IP || appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)) {
|
||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||
if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
||||
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 { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||
@@ -46,6 +47,7 @@ type TDynamicSecretServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||
@@ -60,7 +62,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
dynamicSecretQueueService,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
projectGatewayDAL
|
||||
projectGatewayDAL,
|
||||
resourceMetadataDAL
|
||||
}: TDynamicSecretServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
path,
|
||||
@@ -73,7 +76,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
projectSlug,
|
||||
actorOrgId,
|
||||
defaultTTL,
|
||||
actorAuthMethod
|
||||
actorAuthMethod,
|
||||
metadata
|
||||
}: TCreateDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
@@ -87,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);
|
||||
@@ -131,16 +136,36 @@ export const dynamicSecretServiceFactory = ({
|
||||
projectId
|
||||
});
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
||||
type: provider.type,
|
||||
version: 1,
|
||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
name,
|
||||
projectGatewayId: selectedGatewayId
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||
const cfg = await dynamicSecretDAL.create(
|
||||
{
|
||||
type: provider.type,
|
||||
version: 1,
|
||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
name,
|
||||
projectGatewayId: selectedGatewayId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (metadata) {
|
||||
await resourceMetadataDAL.insertMany(
|
||||
metadata.map(({ key, value }) => ({
|
||||
key,
|
||||
value,
|
||||
dynamicSecretId: cfg.id,
|
||||
orgId: actorOrgId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return cfg;
|
||||
});
|
||||
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
|
||||
@@ -156,7 +181,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
actorId,
|
||||
newName,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
actorAuthMethod,
|
||||
metadata
|
||||
}: TUpdateDynamicSecretDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
@@ -171,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) {
|
||||
@@ -193,6 +215,27 @@ export const dynamicSecretServiceFactory = ({
|
||||
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 (metadata) {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||
environment: environmentSlug,
|
||||
secretPath: path,
|
||||
metadata
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (newName) {
|
||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
||||
if (existingDynamicSecret)
|
||||
@@ -231,14 +274,41 @@ export const dynamicSecretServiceFactory = ({
|
||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }).cipherTextBlob,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
status: null,
|
||||
statusDetails: null,
|
||||
projectGatewayId: selectedGatewayId
|
||||
const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||
const cfg = await dynamicSecretDAL.updateById(
|
||||
dynamicSecretCfg.id,
|
||||
{
|
||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) })
|
||||
.cipherTextBlob,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
status: null,
|
||||
projectGatewayId: selectedGatewayId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (metadata) {
|
||||
await resourceMetadataDAL.delete(
|
||||
{
|
||||
dynamicSecretId: cfg.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
await resourceMetadataDAL.insertMany(
|
||||
metadata.map(({ key, value }) => ({
|
||||
key,
|
||||
value,
|
||||
dynamicSecretId: cfg.id,
|
||||
orgId: actorOrgId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
return cfg;
|
||||
});
|
||||
|
||||
return updatedDynamicCfg;
|
||||
@@ -268,10 +338,6 @@ 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)
|
||||
@@ -282,6 +348,15 @@ export const dynamicSecretServiceFactory = ({
|
||||
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
|
||||
@@ -329,14 +404,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)
|
||||
@@ -346,6 +413,25 @@ export const dynamicSecretServiceFactory = ({
|
||||
if (!dynamicSecretCfg) {
|
||||
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
|
||||
@@ -356,6 +442,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
) as object;
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
||||
|
||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||
};
|
||||
|
||||
@@ -426,7 +513,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);
|
||||
@@ -473,16 +560,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,
|
||||
@@ -490,7 +573,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 (
|
||||
@@ -542,24 +635,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)
|
||||
@@ -572,7 +655,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 ({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
import { DynamicSecretProviderSchema } from "./providers/models";
|
||||
@@ -20,6 +21,7 @@ export type TCreateDynamicSecretDTO = {
|
||||
environmentSlug: string;
|
||||
name: string;
|
||||
projectSlug: string;
|
||||
metadata?: ResourceMetadataDTO;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateDynamicSecretDTO = {
|
||||
@@ -31,6 +33,7 @@ export type TUpdateDynamicSecretDTO = {
|
||||
environmentSlug: string;
|
||||
inputs?: TProvider["inputs"];
|
||||
projectSlug: string;
|
||||
metadata?: ResourceMetadataDTO;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteDynamicSecretDTO = {
|
||||
|
||||
@@ -155,6 +155,10 @@ export type SecretFolderSubjectFields = {
|
||||
export type DynamicSecretSubjectFields = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
metadata?: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type SecretImportSubjectFields = {
|
||||
@@ -284,6 +288,42 @@ const SecretConditionV1Schema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
const DynamicSecretConditionV2Schema = z
|
||||
.object({
|
||||
environment: z.union([
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||
})
|
||||
.partial()
|
||||
]),
|
||||
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA,
|
||||
metadata: z.object({
|
||||
[PermissionConditionOperators.$ELEMENTMATCH]: z
|
||||
.object({
|
||||
key: z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||
})
|
||||
.partial(),
|
||||
value: z
|
||||
.object({
|
||||
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
|
||||
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||
})
|
||||
.partial()
|
||||
})
|
||||
.partial()
|
||||
})
|
||||
})
|
||||
.partial();
|
||||
|
||||
const SecretConditionV2Schema = z
|
||||
.object({
|
||||
environment: z.union([
|
||||
@@ -581,7 +621,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
conditions: SecretConditionV1Schema.describe(
|
||||
conditions: DynamicSecretConditionV2Schema.describe(
|
||||
"When specified, only matching conditions will be allowed to access given resource."
|
||||
).optional()
|
||||
}),
|
||||
|
||||
@@ -24,5 +24,6 @@ export enum PermissionConditionOperators {
|
||||
$IN = "$in",
|
||||
$EQ = "$eq",
|
||||
$NEQ = "$ne",
|
||||
$GLOB = "$glob"
|
||||
$GLOB = "$glob",
|
||||
$ELEMENTMATCH = "$elemMatch"
|
||||
}
|
||||
|
||||
@@ -197,6 +197,7 @@ const envSchema = z
|
||||
/* ----------------------------------------------------------------------------- */
|
||||
|
||||
/* App Connections ----------------------------------------------------------------------------- */
|
||||
ALLOW_INTERNAL_IP_CONNECTIONS: zodStrBool.default("false"),
|
||||
|
||||
// aws
|
||||
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()),
|
||||
|
||||
@@ -118,7 +118,12 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
|
||||
}
|
||||
};
|
||||
|
||||
const $signRsaDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => {
|
||||
const $signRsaDigest = async (
|
||||
digest: Buffer,
|
||||
privateKey: Buffer,
|
||||
hashAlgorithm: SupportedHashAlgorithm,
|
||||
signingAlgorithm: SigningAlgorithm
|
||||
) => {
|
||||
const tempDir = await createTemporaryDirectory("kms-rsa-sign");
|
||||
const digestPath = path.join(tempDir, "digest.bin");
|
||||
const sigPath = path.join(tempDir, "signature.bin");
|
||||
@@ -164,12 +169,22 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
|
||||
}
|
||||
|
||||
return signature;
|
||||
} catch (err) {
|
||||
logger.error(err, "KMS: Failed to sign RSA digest");
|
||||
throw new BadRequestError({
|
||||
message: `Failed to sign RSA digest with ${signingAlgorithm} due to signing error. Ensure that your digest is hashed with ${hashAlgorithm.toUpperCase()}.`
|
||||
});
|
||||
} finally {
|
||||
await cleanTemporaryDirectory(tempDir);
|
||||
}
|
||||
};
|
||||
|
||||
const $signEccDigest = async (digest: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => {
|
||||
const $signEccDigest = async (
|
||||
digest: Buffer,
|
||||
privateKey: Buffer,
|
||||
hashAlgorithm: SupportedHashAlgorithm,
|
||||
signingAlgorithm: SigningAlgorithm
|
||||
) => {
|
||||
const tempDir = await createTemporaryDirectory("ecc-sign");
|
||||
const digestPath = path.join(tempDir, "digest.bin");
|
||||
const keyPath = path.join(tempDir, "key.pem");
|
||||
@@ -216,6 +231,11 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
|
||||
}
|
||||
|
||||
return signature;
|
||||
} catch (err) {
|
||||
logger.error(err, "KMS: Failed to sign ECC digest");
|
||||
throw new BadRequestError({
|
||||
message: `Failed to sign ECC digest with ${signingAlgorithm} due to signing error. Ensure that your digest is hashed with ${hashAlgorithm.toUpperCase()}.`
|
||||
});
|
||||
} finally {
|
||||
await cleanTemporaryDirectory(tempDir);
|
||||
}
|
||||
@@ -329,7 +349,12 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
|
||||
|
||||
const signDigestFunctionsMap: Record<
|
||||
AsymmetricKeyAlgorithm,
|
||||
(data: Buffer, privateKey: Buffer, hashAlgorithm: SupportedHashAlgorithm) => Promise<Buffer>
|
||||
(
|
||||
data: Buffer,
|
||||
privateKey: Buffer,
|
||||
hashAlgorithm: SupportedHashAlgorithm,
|
||||
signingAlgorithm: SigningAlgorithm
|
||||
) => Promise<Buffer>
|
||||
> = {
|
||||
[AsymmetricKeyAlgorithm.ECC_NIST_P256]: $signEccDigest,
|
||||
[AsymmetricKeyAlgorithm.RSA_4096]: $signRsaDigest
|
||||
@@ -360,7 +385,7 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
|
||||
});
|
||||
}
|
||||
|
||||
const signature = await signFunction(data, privateKey, hashAlgorithm);
|
||||
const signature = await signFunction(data, privateKey, hashAlgorithm, signingAlgorithm);
|
||||
return signature;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,16 @@ import dns from "node:dns/promises";
|
||||
|
||||
import { isIPv4 } from "net";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
|
||||
import { BadRequestError } from "../errors";
|
||||
import { isPrivateIp } from "../ip/ipRange";
|
||||
|
||||
export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (appCfg.isDevelopmentMode) return;
|
||||
|
||||
const validUrl = new URL(url);
|
||||
const inputHostIps: string[] = [];
|
||||
if (isIPv4(validUrl.host)) {
|
||||
@@ -18,7 +24,8 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
||||
inputHostIps.push(...resolvedIps);
|
||||
}
|
||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||
if (isInternalIp) throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||
if (isInternalIp && !appCfg.ALLOW_INTERNAL_IP_CONNECTIONS)
|
||||
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||
};
|
||||
|
||||
type FQDNOptions = {
|
||||
|
||||
@@ -1391,7 +1391,8 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService,
|
||||
projectGatewayDAL
|
||||
projectGatewayDAL,
|
||||
resourceMetadataDAL
|
||||
});
|
||||
|
||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
|
||||
import { UnpackedPermissionSchema } from "./sanitizedSchema/permission";
|
||||
|
||||
@@ -232,7 +233,11 @@ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
||||
inputIV: true,
|
||||
inputTag: true,
|
||||
algorithm: true
|
||||
});
|
||||
}).merge(
|
||||
z.object({
|
||||
metadata: ResourceMetadataSchema.optional()
|
||||
})
|
||||
);
|
||||
|
||||
export const SanitizedAuditLogStreamSchema = z.object({
|
||||
id: z.string(),
|
||||
|
||||
@@ -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";
|
||||
@@ -317,24 +313,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,
|
||||
@@ -343,7 +322,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId,
|
||||
search,
|
||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
||||
environmentSlugs: environments,
|
||||
path: secretPath,
|
||||
isInternal: true
|
||||
});
|
||||
@@ -358,7 +337,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
||||
environmentSlugs: environments,
|
||||
path: secretPath,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset,
|
||||
|
||||
@@ -39,17 +39,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.CREATE.path),
|
||||
.describe(FOLDERS.CREATE.path)
|
||||
.optional(),
|
||||
// backward compatiability with cli
|
||||
directory: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.CREATE.directory),
|
||||
.describe(FOLDERS.CREATE.directory)
|
||||
.optional(),
|
||||
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
|
||||
}),
|
||||
response: {
|
||||
@@ -60,7 +62,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const path = req.body.path || req.body.directory;
|
||||
const path = req.body.path || req.body.directory || "/";
|
||||
const folder = await server.services.folder.createFolder({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
@@ -120,17 +122,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.UPDATE.path),
|
||||
.describe(FOLDERS.UPDATE.path)
|
||||
.optional(),
|
||||
// backward compatiability with cli
|
||||
directory: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.UPDATE.directory),
|
||||
.describe(FOLDERS.UPDATE.directory)
|
||||
.optional(),
|
||||
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
|
||||
}),
|
||||
response: {
|
||||
@@ -141,7 +145,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const path = req.body.path || req.body.directory;
|
||||
const path = req.body.path || req.body.directory || "/";
|
||||
const { folder, old } = await server.services.folder.updateFolder({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
@@ -271,17 +275,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.DELETE.path),
|
||||
.describe(FOLDERS.DELETE.path)
|
||||
.optional(),
|
||||
// keep this here as cli need directory
|
||||
directory: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.DELETE.directory)
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -291,7 +297,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const path = req.body.path || req.body.directory;
|
||||
const path = req.body.path || req.body.directory || "/";
|
||||
const folder = await server.services.folder.deleteFolder({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
@@ -339,18 +345,18 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.LIST.path),
|
||||
.describe(FOLDERS.LIST.path)
|
||||
.optional(),
|
||||
// backward compatiability with cli
|
||||
directory: z
|
||||
.string()
|
||||
.trim()
|
||||
.default("/")
|
||||
.transform(prefixWithSlash)
|
||||
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(FOLDERS.LIST.directory),
|
||||
.describe(FOLDERS.LIST.directory)
|
||||
.optional(),
|
||||
recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive)
|
||||
}),
|
||||
response: {
|
||||
@@ -363,7 +369,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const path = req.query.path || req.query.directory;
|
||||
const path = req.query.path || req.query.directory || "/";
|
||||
const folders = await server.services.folder.getFolders({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
|
||||
@@ -819,16 +819,17 @@ export const secretImportServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||
);
|
||||
if (
|
||||
permission.cannot(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||
)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
throw new NotFoundError({
|
||||
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`
|
||||
});
|
||||
if (!folder) return [];
|
||||
|
||||
const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId);
|
||||
const deepPaths: { path: string; folderId: string }[] = [];
|
||||
|
||||
Reference in New Issue
Block a user