Merge branch 'main' into feat/addProjectDeletionProtection
@@ -22,3 +22,5 @@ frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredent
|
|||||||
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:28
|
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:28
|
||||||
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
|
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
|
||||||
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
||||||
|
docs/documentation/platform/kms/overview.mdx:generic-api-key:281
|
||||||
|
docs/documentation/platform/kms/overview.mdx:generic-api-key:344
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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(),
|
identityId: z.string().uuid().nullable().optional(),
|
||||||
secretId: z.string().uuid().nullable().optional(),
|
secretId: z.string().uuid().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
dynamicSecretId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TResourceMetadata = z.infer<typeof ResourceMetadataSchema>;
|
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||||
|
|
||||||
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -48,7 +49,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
.nullable(),
|
.nullable(),
|
||||||
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
||||||
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
|
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: {
|
response: {
|
||||||
200: z.object({
|
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" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
})
|
})
|
||||||
.nullable(),
|
.nullable(),
|
||||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional()
|
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
|
||||||
|
metadata: ResourceMetadataSchema.optional()
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -238,6 +241,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
name: req.params.name,
|
name: req.params.name,
|
||||||
...req.query
|
...req.query
|
||||||
});
|
});
|
||||||
|
|
||||||
return { dynamicSecret: dynamicSecretCfg };
|
return { dynamicSecret: dynamicSecretCfg };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -78,10 +78,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan?.dynamicSecret) {
|
if (!plan?.dynamicSecret) {
|
||||||
@@ -102,6 +98,15 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
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);
|
const totalLeasesTaken = await dynamicSecretLeaseDAL.countLeasesForDynamicSecret(dynamicSecretCfg.id);
|
||||||
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
|
if (totalLeasesTaken >= appCfg.MAX_LEASE_LIMIT)
|
||||||
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${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,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
@@ -187,7 +188,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
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 selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const decryptedStoredInput = JSON.parse(
|
const decryptedStoredInput = JSON.parse(
|
||||||
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
||||||
@@ -239,10 +258,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
@@ -259,7 +274,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id)
|
if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id)
|
||||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
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 selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const decryptedStoredInput = JSON.parse(
|
const decryptedStoredInput = JSON.parse(
|
||||||
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
secretManagerDecryptor({ cipherTextBlob: Buffer.from(dynamicSecretCfg.encryptedInput) }).toString()
|
||||||
@@ -309,10 +342,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
@@ -326,6 +355,15 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
message: `Dynamic secret with name '${name}' in folder with path '${path}' not found`
|
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 });
|
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
||||||
return dynamicSecretLeases;
|
return dynamicSecretLeases;
|
||||||
};
|
};
|
||||||
@@ -352,10 +390,6 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.Lease,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' not found` });
|
if (!folder) throw new NotFoundError({ message: `Folder with path '${path}' not found` });
|
||||||
@@ -364,6 +398,25 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
if (!dynamicSecretLease)
|
if (!dynamicSecretLease)
|
||||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
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;
|
return dynamicSecretLease;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
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 { 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 { OrderByDirection } from "@app/lib/types";
|
||||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
@@ -12,6 +20,86 @@ export type TDynamicSecretDALFactory = ReturnType<typeof dynamicSecretDALFactory
|
|||||||
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
export const dynamicSecretDALFactory = (db: TDbClient) => {
|
||||||
const orm = ormify(db, TableName.DynamicSecret);
|
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)
|
// find dynamic secrets for multiple environments (folder IDs are cross env, thus need to rank for pagination)
|
||||||
const listDynamicSecretsByFolderIds = async (
|
const listDynamicSecretsByFolderIds = async (
|
||||||
{
|
{
|
||||||
@@ -39,18 +127,27 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
|||||||
void bd.whereILike(`${TableName.DynamicSecret}.name`, `%${search}%`);
|
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.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
|
||||||
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
.select(
|
.select(
|
||||||
selectAllTableCols(TableName.DynamicSecret),
|
selectAllTableCols(TableName.DynamicSecret),
|
||||||
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
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);
|
.orderBy(`${TableName.DynamicSecret}.${orderBy}`, orderDirection);
|
||||||
|
|
||||||
|
let queryWithLimit;
|
||||||
if (limit) {
|
if (limit) {
|
||||||
const rankOffset = offset + 1;
|
const rankOffset = offset + 1;
|
||||||
return await (tx || db)
|
queryWithLimit = (tx || db.replicaNode())
|
||||||
.with("w", query)
|
.with("w", query)
|
||||||
.select("*")
|
.select("*")
|
||||||
.from<Awaited<typeof query>[number]>("w")
|
.from<Awaited<typeof query>[number]>("w")
|
||||||
@@ -58,7 +155,22 @@ export const dynamicSecretDALFactory = (db: TDbClient) => {
|
|||||||
.andWhere("w.rank", "<", rankOffset + limit);
|
.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;
|
return dynamicSecrets;
|
||||||
} catch (error) {
|
} 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);
|
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));
|
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||||
if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" });
|
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 { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
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 { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
|
||||||
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||||
@@ -46,6 +47,7 @@ type TDynamicSecretServiceFactoryDep = {
|
|||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
||||||
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||||
@@ -60,7 +62,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
dynamicSecretQueueService,
|
dynamicSecretQueueService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectGatewayDAL
|
projectGatewayDAL,
|
||||||
|
resourceMetadataDAL
|
||||||
}: TDynamicSecretServiceFactoryDep) => {
|
}: TDynamicSecretServiceFactoryDep) => {
|
||||||
const create = async ({
|
const create = async ({
|
||||||
path,
|
path,
|
||||||
@@ -73,7 +76,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
projectSlug,
|
projectSlug,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
actorAuthMethod
|
actorAuthMethod,
|
||||||
|
metadata
|
||||||
}: TCreateDynamicSecretDTO) => {
|
}: TCreateDynamicSecretDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
@@ -87,9 +91,10 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path, metadata })
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
@@ -131,16 +136,36 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
const dynamicSecretCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||||
type: provider.type,
|
const cfg = await dynamicSecretDAL.create(
|
||||||
version: 1,
|
{
|
||||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
|
type: provider.type,
|
||||||
maxTTL,
|
version: 1,
|
||||||
defaultTTL,
|
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob,
|
||||||
folderId: folder.id,
|
maxTTL,
|
||||||
name,
|
defaultTTL,
|
||||||
projectGatewayId: selectedGatewayId
|
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;
|
return dynamicSecretCfg;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,7 +181,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
newName,
|
newName,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod,
|
||||||
|
metadata
|
||||||
}: TUpdateDynamicSecretDTO) => {
|
}: TUpdateDynamicSecretDTO) => {
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||||
@@ -171,10 +197,6 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.EditRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(actorOrgId);
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
if (!plan?.dynamicSecret) {
|
if (!plan?.dynamicSecret) {
|
||||||
@@ -193,6 +215,27 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found`
|
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) {
|
if (newName) {
|
||||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
||||||
if (existingDynamicSecret)
|
if (existingDynamicSecret)
|
||||||
@@ -231,14 +274,41 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
|
||||||
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
const updatedDynamicCfg = await dynamicSecretDAL.transaction(async (tx) => {
|
||||||
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) }).cipherTextBlob,
|
const cfg = await dynamicSecretDAL.updateById(
|
||||||
maxTTL,
|
dynamicSecretCfg.id,
|
||||||
defaultTTL,
|
{
|
||||||
name: newName ?? name,
|
encryptedInput: secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(updatedInput)) })
|
||||||
status: null,
|
.cipherTextBlob,
|
||||||
statusDetails: null,
|
maxTTL,
|
||||||
projectGatewayId: selectedGatewayId
|
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;
|
return updatedDynamicCfg;
|
||||||
@@ -268,10 +338,6 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
@@ -282,6 +348,15 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `Dynamic secret with name '${name}' in folder '${folder.path}' not found` });
|
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 });
|
const leases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfg.id });
|
||||||
// when not forced we check with the external system to first remove the things
|
// 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
|
// 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,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
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);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
@@ -346,6 +413,25 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
if (!dynamicSecretCfg) {
|
if (!dynamicSecretCfg) {
|
||||||
throw new NotFoundError({ message: `Dynamic secret with name '${name} in folder '${path}' not found` });
|
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({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId
|
projectId
|
||||||
@@ -356,6 +442,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
) as object;
|
) as object;
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
||||||
|
|
||||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -426,7 +513,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
ProjectPermissionSub.DynamicSecrets
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
@@ -473,16 +560,12 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment: environmentSlug, secretPath: path })
|
|
||||||
);
|
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, path);
|
||||||
if (!folder)
|
if (!folder)
|
||||||
throw new NotFoundError({ message: `Folder with path '${path}' in environment '${environmentSlug}' not found` });
|
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 },
|
{ folderId: folder.id, $search: search ? { name: `%${search}%` } : undefined },
|
||||||
{
|
{
|
||||||
limit,
|
limit,
|
||||||
@@ -490,7 +573,17 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
sort: orderBy ? [[orderBy, orderDirection]] : undefined
|
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 (
|
const listDynamicSecretsByFolderIds = async (
|
||||||
@@ -542,24 +635,14 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
isInternal,
|
isInternal,
|
||||||
...params
|
...params
|
||||||
}: TListDynamicSecretsMultiEnvDTO) => {
|
}: TListDynamicSecretsMultiEnvDTO) => {
|
||||||
if (!isInternal) {
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
actor,
|
||||||
actor,
|
actorId,
|
||||||
actorId,
|
projectId,
|
||||||
projectId,
|
actorAuthMethod,
|
||||||
actorAuthMethod,
|
actorOrgId,
|
||||||
actorOrgId,
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
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 folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environmentSlugs, path);
|
||||||
if (!folders.length)
|
if (!folders.length)
|
||||||
@@ -572,7 +655,16 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
...params
|
...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 ({
|
const fetchAzureEntraIdUsers = async ({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
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 { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||||
|
|
||||||
import { DynamicSecretProviderSchema } from "./providers/models";
|
import { DynamicSecretProviderSchema } from "./providers/models";
|
||||||
@@ -20,6 +21,7 @@ export type TCreateDynamicSecretDTO = {
|
|||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
name: string;
|
name: string;
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
metadata?: ResourceMetadataDTO;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateDynamicSecretDTO = {
|
export type TUpdateDynamicSecretDTO = {
|
||||||
@@ -31,6 +33,7 @@ export type TUpdateDynamicSecretDTO = {
|
|||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
inputs?: TProvider["inputs"];
|
inputs?: TProvider["inputs"];
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
metadata?: ResourceMetadataDTO;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TDeleteDynamicSecretDTO = {
|
export type TDeleteDynamicSecretDTO = {
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ export type SecretFolderSubjectFields = {
|
|||||||
export type DynamicSecretSubjectFields = {
|
export type DynamicSecretSubjectFields = {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
|
metadata?: {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SecretImportSubjectFields = {
|
export type SecretImportSubjectFields = {
|
||||||
@@ -284,6 +288,42 @@ const SecretConditionV1Schema = z
|
|||||||
})
|
})
|
||||||
.partial();
|
.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
|
const SecretConditionV2Schema = z
|
||||||
.object({
|
.object({
|
||||||
environment: z.union([
|
environment: z.union([
|
||||||
@@ -581,7 +621,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
|||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionDynamicSecretActions).describe(
|
||||||
"Describe what action an entity can take."
|
"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."
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
).optional()
|
).optional()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -24,5 +24,6 @@ export enum PermissionConditionOperators {
|
|||||||
$IN = "$in",
|
$IN = "$in",
|
||||||
$EQ = "$eq",
|
$EQ = "$eq",
|
||||||
$NEQ = "$ne",
|
$NEQ = "$ne",
|
||||||
$GLOB = "$glob"
|
$GLOB = "$glob",
|
||||||
|
$ELEMENTMATCH = "$elemMatch"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ const envSchema = z
|
|||||||
/* ----------------------------------------------------------------------------- */
|
/* ----------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/* App Connections ----------------------------------------------------------------------------- */
|
/* App Connections ----------------------------------------------------------------------------- */
|
||||||
|
ALLOW_INTERNAL_IP_CONNECTIONS: zodStrBool.default("false"),
|
||||||
|
|
||||||
// aws
|
// aws
|
||||||
INF_APP_CONNECTION_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()),
|
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 tempDir = await createTemporaryDirectory("kms-rsa-sign");
|
||||||
const digestPath = path.join(tempDir, "digest.bin");
|
const digestPath = path.join(tempDir, "digest.bin");
|
||||||
const sigPath = path.join(tempDir, "signature.bin");
|
const sigPath = path.join(tempDir, "signature.bin");
|
||||||
@@ -164,12 +169,22 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
|
|||||||
}
|
}
|
||||||
|
|
||||||
return signature;
|
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 {
|
} finally {
|
||||||
await cleanTemporaryDirectory(tempDir);
|
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 tempDir = await createTemporaryDirectory("ecc-sign");
|
||||||
const digestPath = path.join(tempDir, "digest.bin");
|
const digestPath = path.join(tempDir, "digest.bin");
|
||||||
const keyPath = path.join(tempDir, "key.pem");
|
const keyPath = path.join(tempDir, "key.pem");
|
||||||
@@ -216,6 +231,11 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
|
|||||||
}
|
}
|
||||||
|
|
||||||
return signature;
|
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 {
|
} finally {
|
||||||
await cleanTemporaryDirectory(tempDir);
|
await cleanTemporaryDirectory(tempDir);
|
||||||
}
|
}
|
||||||
@@ -329,7 +349,12 @@ export const signingService = (algorithm: AsymmetricKeyAlgorithm): TAsymmetricSi
|
|||||||
|
|
||||||
const signDigestFunctionsMap: Record<
|
const signDigestFunctionsMap: Record<
|
||||||
AsymmetricKeyAlgorithm,
|
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.ECC_NIST_P256]: $signEccDigest,
|
||||||
[AsymmetricKeyAlgorithm.RSA_4096]: $signRsaDigest
|
[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;
|
return signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ import dns from "node:dns/promises";
|
|||||||
|
|
||||||
import { isIPv4 } from "net";
|
import { isIPv4 } from "net";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
import { BadRequestError } from "../errors";
|
import { BadRequestError } from "../errors";
|
||||||
import { isPrivateIp } from "../ip/ipRange";
|
import { isPrivateIp } from "../ip/ipRange";
|
||||||
|
|
||||||
export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
if (appCfg.isDevelopmentMode) return;
|
||||||
|
|
||||||
const validUrl = new URL(url);
|
const validUrl = new URL(url);
|
||||||
const inputHostIps: string[] = [];
|
const inputHostIps: string[] = [];
|
||||||
if (isIPv4(validUrl.host)) {
|
if (isIPv4(validUrl.host)) {
|
||||||
@@ -18,7 +24,8 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
|||||||
inputHostIps.push(...resolvedIps);
|
inputHostIps.push(...resolvedIps);
|
||||||
}
|
}
|
||||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
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 = {
|
type FQDNOptions = {
|
||||||
|
|||||||
@@ -1391,7 +1391,8 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectGatewayDAL
|
projectGatewayDAL,
|
||||||
|
resourceMetadataDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
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";
|
import { UnpackedPermissionSchema } from "./sanitizedSchema/permission";
|
||||||
|
|
||||||
@@ -232,7 +233,11 @@ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
|||||||
inputIV: true,
|
inputIV: true,
|
||||||
inputTag: true,
|
inputTag: true,
|
||||||
algorithm: true
|
algorithm: true
|
||||||
});
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
metadata: ResourceMetadataSchema.optional()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const SanitizedAuditLogStreamSchema = z.object({
|
export const SanitizedAuditLogStreamSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import { z } from "zod";
|
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 { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import {
|
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
|
||||||
ProjectPermissionDynamicSecretActions,
|
|
||||||
ProjectPermissionSecretActions,
|
|
||||||
ProjectPermissionSub
|
|
||||||
} from "@app/ee/services/permission/project-permission";
|
|
||||||
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
|
||||||
import { DASHBOARD } from "@app/lib/api-docs";
|
import { DASHBOARD } from "@app/lib/api-docs";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
@@ -317,24 +313,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalCount: totalFolderCount ?? 0
|
totalCount: totalFolderCount ?? 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const { permission } = await server.services.permission.getProjectPermission({
|
if (includeDynamicSecrets) {
|
||||||
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) {
|
|
||||||
// this is the unique count, ie duplicate secrets across envs only count as 1
|
// this is the unique count, ie duplicate secrets across envs only count as 1
|
||||||
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
totalDynamicSecretCount = await server.services.dynamicSecret.getCountMultiEnv({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -343,7 +322,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId,
|
projectId,
|
||||||
search,
|
search,
|
||||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
environmentSlugs: environments,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
isInternal: true
|
isInternal: true
|
||||||
});
|
});
|
||||||
@@ -358,7 +337,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
search,
|
search,
|
||||||
orderBy,
|
orderBy,
|
||||||
orderDirection,
|
orderDirection,
|
||||||
environmentSlugs: allowedDynamicSecretEnvironments,
|
environmentSlugs: environments,
|
||||||
path: secretPath,
|
path: secretPath,
|
||||||
limit: remainingLimit,
|
limit: remainingLimit,
|
||||||
offset: adjustedOffset,
|
offset: adjustedOffset,
|
||||||
|
|||||||
@@ -39,17 +39,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.CREATE.path),
|
.describe(FOLDERS.CREATE.path)
|
||||||
|
.optional(),
|
||||||
// backward compatiability with cli
|
// backward compatiability with cli
|
||||||
directory: z
|
directory: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.CREATE.directory),
|
.describe(FOLDERS.CREATE.directory)
|
||||||
|
.optional(),
|
||||||
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
|
description: z.string().optional().nullable().describe(FOLDERS.CREATE.description)
|
||||||
}),
|
}),
|
||||||
response: {
|
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]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
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({
|
const folder = await server.services.folder.createFolder({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -120,17 +122,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.UPDATE.path),
|
.describe(FOLDERS.UPDATE.path)
|
||||||
|
.optional(),
|
||||||
// backward compatiability with cli
|
// backward compatiability with cli
|
||||||
directory: z
|
directory: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.UPDATE.directory),
|
.describe(FOLDERS.UPDATE.directory)
|
||||||
|
.optional(),
|
||||||
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
|
description: z.string().optional().nullable().describe(FOLDERS.UPDATE.description)
|
||||||
}),
|
}),
|
||||||
response: {
|
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]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
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({
|
const { folder, old } = await server.services.folder.updateFolder({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -271,17 +275,19 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.DELETE.path),
|
.describe(FOLDERS.DELETE.path)
|
||||||
|
.optional(),
|
||||||
// keep this here as cli need directory
|
// keep this here as cli need directory
|
||||||
directory: z
|
directory: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.default("/")
|
||||||
.transform(prefixWithSlash)
|
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.DELETE.directory)
|
.describe(FOLDERS.DELETE.directory)
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
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]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
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({
|
const folder = await server.services.folder.deleteFolder({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -339,18 +345,18 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
|
|||||||
path: z
|
path: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
|
||||||
.transform(prefixWithSlash)
|
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.LIST.path),
|
.describe(FOLDERS.LIST.path)
|
||||||
|
.optional(),
|
||||||
// backward compatiability with cli
|
// backward compatiability with cli
|
||||||
directory: z
|
directory: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.default("/")
|
.transform(prefixWithSlash) // Transformations get skipped if directory is undefined
|
||||||
.transform(prefixWithSlash)
|
|
||||||
.transform(removeTrailingSlash)
|
.transform(removeTrailingSlash)
|
||||||
.describe(FOLDERS.LIST.directory),
|
.describe(FOLDERS.LIST.directory)
|
||||||
|
.optional(),
|
||||||
recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive)
|
recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive)
|
||||||
}),
|
}),
|
||||||
response: {
|
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]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
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({
|
const folders = await server.services.folder.getFolders({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
|
|||||||
@@ -819,16 +819,17 @@ export const secretImportServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
if (
|
||||||
ProjectPermissionActions.Read,
|
permission.cannot(
|
||||||
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
ProjectPermissionActions.Read,
|
||||||
);
|
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
if (!folder)
|
if (!folder) return [];
|
||||||
throw new NotFoundError({
|
|
||||||
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`
|
|
||||||
});
|
|
||||||
|
|
||||||
const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId);
|
const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId);
|
||||||
const deepPaths: { path: string; folderId: string }[] = [];
|
const deepPaths: { path: string; folderId: string }[] = [];
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
Maximum time-to-live for a generated secret
|
Maximum time-to-live for a generated secret
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Metadata" type="list" required>
|
||||||
|
List of key/value metadata pairs
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Service" type="string" required>
|
<ParamField path="Service" type="string" required>
|
||||||
Choose the service you want to generate dynamic secrets for. This must be selected as **MS SQL**.
|
Choose the service you want to generate dynamic secrets for. This must be selected as **MS SQL**.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
Maximum time-to-live for a generated secret
|
Maximum time-to-live for a generated secret
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Metadata" type="list" required>
|
||||||
|
List of key/value metadata pairs
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Service" type="string" required>
|
<ParamField path="Service" type="string" required>
|
||||||
Choose the service you want to generate dynamic secrets for. This must be selected as **MySQL**.
|
Choose the service you want to generate dynamic secrets for. This must be selected as **MySQL**.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
Maximum time-to-live for a generated secret
|
Maximum time-to-live for a generated secret
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Metadata" type="list" required>
|
||||||
|
List of key/value metadata pairs
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Service" type="string" required>
|
<ParamField path="Service" type="string" required>
|
||||||
Choose the service you want to generate dynamic secrets for. This must be selected as **Oracle**.
|
Choose the service you want to generate dynamic secrets for. This must be selected as **Oracle**.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
@@ -62,7 +66,7 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions).
|
A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions).
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="(Optional) Modify SQL Statements">
|
<Step title="(Optional) Modify SQL Statements">
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
Maximum time-to-live for a generated secret
|
Maximum time-to-live for a generated secret
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Metadata" type="list" required>
|
||||||
|
List of key/value metadata pairs
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="Service" type="string" required>
|
<ParamField path="Service" type="string" required>
|
||||||
Choose the service you want to generate dynamic secrets for. This must be selected as **PostgreSQL**.
|
Choose the service you want to generate dynamic secrets for. This must be selected as **PostgreSQL**.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
@@ -63,7 +67,7 @@ Create a user with the required permission in your SQL instance. This user will
|
|||||||
A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions).
|
A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions).
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="(Optional) Modify SQL Statements">
|
<Step title="(Optional) Modify SQL Statements">
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ The typical workflow for using Infisical KMS consists of the following steps:
|
|||||||
as via API.
|
as via API.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
## Guide to Encrypting Data
|
## Encryption
|
||||||
|
|
||||||
|
### Guide to Encrypting Data
|
||||||
|
|
||||||
In the following steps, we explore how to generate a key and use it to encrypt data.
|
In the following steps, we explore how to generate a key and use it to encrypt data.
|
||||||
|
|
||||||
@@ -44,7 +46,8 @@ In the following steps, we explore how to generate a key and use it to encrypt d
|
|||||||
Specify your key details. Here's some guidance on each field:
|
Specify your key details. Here's some guidance on each field:
|
||||||
|
|
||||||
- Name: A slug-friendly name for the key.
|
- Name: A slug-friendly name for the key.
|
||||||
- Type: The encryption algorithm associated with the key (e.g. `AES-GCM-256`).
|
- Key Usage: The type of key to create (e.g `Encrypt/Decrypt` for encryption, and `Sign/Verify` for signing).
|
||||||
|
- Algorithm: The encryption algorithm associated with the key (e.g. `AES-GCM-256`).
|
||||||
- Description: An optional description of what the intended usage is for the key.
|
- Description: An optional description of what the intended usage is for the key.
|
||||||
|
|
||||||

|

|
||||||
@@ -137,7 +140,7 @@ In the following steps, we explore how to generate a key and use it to encrypt d
|
|||||||
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
## Guide to Decrypting Data
|
### Guide to Decrypting Data
|
||||||
|
|
||||||
In the following steps, we explore how to use decrypt data using an existing key in Infisical KMS.
|
In the following steps, we explore how to use decrypt data using an existing key in Infisical KMS.
|
||||||
|
|
||||||
@@ -193,6 +196,164 @@ In the following steps, we explore how to use decrypt data using an existing key
|
|||||||
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
## Signing
|
||||||
|
|
||||||
|
### Guide to Signing Data
|
||||||
|
|
||||||
|
In the following steps, we explore how to generate a key and use it to sign data.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Infisical UI">
|
||||||
|
<Steps>
|
||||||
|
<Step title="Creating a KMS key">
|
||||||
|
Navigate to Project > Key Management and tap on the **Add Key** button.
|
||||||
|

|
||||||
|
|
||||||
|
Specify your key details. Here's some guidance on each field:
|
||||||
|
|
||||||
|
- Name: A slug-friendly name for the key.
|
||||||
|
- Key Usage: The type of key to create (e.g `Encrypt/Decrypt` for encryption, and `Sign/Verify` for signing).
|
||||||
|
- Algorithm: The signing algorithm associated with the key (e.g. `RSA_4096`).
|
||||||
|
- Description: An optional description of what the intended usage is for the key.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Signing data with the KMS key">
|
||||||
|
Once your key is generated, open the options menu for the newly created key and select sign data.
|
||||||
|

|
||||||
|
|
||||||
|
Populate the text area with your data and tap on the Sign button.
|
||||||
|

|
||||||
|
|
||||||
|
Make sure to select the appropriate signing algorithm that will be used to sign the data.
|
||||||
|
Supported signing algorithms are:
|
||||||
|
|
||||||
|
**For RSA keys:**
|
||||||
|
- `RSASSA PSS SHA 512`: Not deterministic, and includes random salt.
|
||||||
|
- `RSASSA PSS SHA 384`: Not deterministic, and includes random salt.
|
||||||
|
- `RSASSA PSS SHA 256`: Not deterministic, and includes random salt.
|
||||||
|
- `RSASSA PKCS1 V1.5 SHA 512`: Deterministic, and does not include randomness.
|
||||||
|
- `RSASSA PKCS1 V1.5 SHA 384`: Deterministic, and does not include randomness.
|
||||||
|
- `RSASSA PKCS1 V1.5 SHA 256`: Deterministic, and does not include randomness.
|
||||||
|
|
||||||
|
**For ECC keys:**
|
||||||
|
- `ECDSA SHA 512`: Not deterministic, and includes randomness.
|
||||||
|
- `ECDSA SHA 384`: Not deterministic, and includes randomness.
|
||||||
|
- `ECDSA SHA 256`: Not deterministic, and includes randomness.
|
||||||
|
|
||||||
|
In this example, we'll use the `RSASSA PSS SHA 512` signing algorithm.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If your data is already Base64 encoded make sure to toggle the respective switch on to avoid
|
||||||
|
redundant encoding.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
Copy and store the signature of your data.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="API">
|
||||||
|
<Steps>
|
||||||
|
<Step title="Signing data">
|
||||||
|
To sign data, make an API request to the [Sign
|
||||||
|
Data](/api-reference/endpoints/kms/signing/sign) API endpoint,
|
||||||
|
specifying the key to use.
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --request POST \
|
||||||
|
--url https://app.infisical.com/api/v1/kms/keys/<key-id>/sign \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"data": "SGVsbG8sIFdvcmxkIQ==", // base64 encoded data
|
||||||
|
"signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512",
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```bash Response
|
||||||
|
{
|
||||||
|
"signature": "JYuiBt1Ta9pbqFIW9Ou6qzBsFhjYbMJp9k4dP87ILrO+F2MPnp85g3nOlXK1ttZmRoGWsWnLNDRn9W3rf5VtkeaixPqUW/KvY/fM3CxdMyIV3BuxlGgDksjL8X34Eqkrz4CCPo9hjB5uT+rBCOxCgZqRbOdATPipAneUapI9npseNquEeh3jPklwviBix83PJHV9PW2t03AGGUXuMY55ZaFEIMv+IrI1WYdnPVIXDyIitYsS3y+/6KRfhVeTcPNJ5Rw+FE9y1eZzDEZtTNpxOfUT3QIoXmpZlYL4HbhRuJBZ+Yx54C7uPiUIN9U69XbyXt+Kkynykw2HPaagwuCZxiqCU5sFfLnrVbc3dmZxQcX2yRrs2gmFamzBx+uVbi648H4mb7WuE5UPTBjjA11jRsBjCY0YS2T4Vgfe1RlzlPQkZgjP/bnCCGDqXa3/VZAlZX1nTI51X995bPHBQI0rq2sNDlIXenwiAy1wJSITbSI8DbUx09Cr83xCEaYAE6R6PUfog/tbIUXi0VbrYsCVkAGCK446Wb1vW6q7HR8jrjXNwmXlqN9eLbSVWqdWj7N7fieeTYSrECtUaAjxtUYTIVsH2bfT6FOEM9gMWKffOpFowVzzr3B9bNQLIhnEEwebxBw947i4OcxyVIcEUuumWxoKvcbSPxzJ8v1M3SoBBh4=", // base64 encoded signature
|
||||||
|
"keyId": "62b2c14e-58af-4199-9842-02995c63edf9",
|
||||||
|
"signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
To sign predigested data, you can pass `"isDigest": true` in the request body. This requires the data to be a base64 encoded digest of the data you wish to sign.
|
||||||
|
It's important that the digest is created using the same hashing algorithm as the signing algorithm. As an example, you would create the digest with `SHA512` if you are using the `RSASSA_PKCS1_V1_5_SHA_512` signing algorithm.
|
||||||
|
</Note>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### Guide to Verifying Data
|
||||||
|
|
||||||
|
In the following steps, we explore how to verify data using an existing key in Infisical KMS.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Infisical UI">
|
||||||
|
<Steps>
|
||||||
|
<Step title="Accessing your key">
|
||||||
|
Navigate to Project > Key Management and open the options menu for the key used to sign the data
|
||||||
|
you want to verify.
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Verifying data with the KMS key">
|
||||||
|
Paste your signature and data into the text areas and tap on the Verify button.
|
||||||
|

|
||||||
|
|
||||||
|
Your verification result will be displayed and can be copied for use.
|
||||||
|

|
||||||
|
|
||||||
|
If the signature is invalid, you'll see an error message indicating that the signature is invalid, and the "Signature Status" field will be `Invalid`.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="API">
|
||||||
|
<Steps>
|
||||||
|
<Step title="Verifying data">
|
||||||
|
To verify data, make an API request to the [Verify
|
||||||
|
Data](/api-reference/endpoints/kms/signing/verify) API endpoint,
|
||||||
|
specifying the key to use.
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --request POST \
|
||||||
|
--url https://app.infisical.com/api/v1/kms/keys/<key-id>/verify \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"data": "SGVsbG8sIFdvcmxkIQ==", // base64 encoded data
|
||||||
|
"signature": "JYuiBt1Ta9pbqFIW9Ou6qzBsFhjYbMJp9k4dP87ILrO+F2MPnp85g3nOlXK1ttZmRoGWsWnLNDRn9W3rf5VtkeaixPqUW/KvY/fM3CxdMyIV3BuxlGgDksjL8X34Eqkrz4CCPo9hjB5uT+rBCOxCgZqRbOdATPipAneUapI9npseNquEeh3jPklwviBix83PJHV9PW2t03AGGUXuMY55ZaFEIMv+IrI1WYdnPVIXDyIitYsS3y+/6KRfhVeTcPNJ5Rw+FE9y1eZzDEZtTNpxOfUT3QIoXmpZlYL4HbhRuJBZ+Yx54C7uPiUIN9U69XbyXt+Kkynykw2HPaagwuCZxiqCU5sFfLnrVbc3dmZxQcX2yRrs2gmFamzBx+uVbi648H4mb7WuE5UPTBjjA11jRsBjCY0YS2T4Vgfe1RlzlPQkZgjP/bnCCGDqXa3/VZAlZX1nTI51X995bPHBQI0rq2sNDlIXenwiAy1wJSITbSI8DbUx09Cr83xCEaYAE6R6PUfog/tbIUXi0VbrYsCVkAGCK446Wb1vW6q7HR8jrjXNwmXlqN9eLbSVWqdWj7N7fieeTYSrECtUaAjxtUYTIVsH2bfT6FOEM9gMWKffOpFowVzzr3B9bNQLIhnEEwebxBw947i4OcxyVIcEUuumWxoKvcbSPxzJ8v1M3SoBBh4=", // base64 encoded signature
|
||||||
|
"signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```bash Response
|
||||||
|
{
|
||||||
|
"signatureValid": true,
|
||||||
|
"keyId": "62b2c14e-58af-4199-9842-02995c63edf9",
|
||||||
|
"signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
<Note>
|
||||||
|
To verify predigested data, you can pass `"isDigest": true` in the request body. This requires the data to be a base64 encoded digest of the data you wish to verify.
|
||||||
|
It's important that the digest is created using the same hashing algorithm as the signing algorithm. As an example, you would create the digest with `SHA512` if you are using the `RSASSA_PKCS1_V1_5_SHA_512` signing algorithm.
|
||||||
|
</Note>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
<AccordionGroup>
|
<AccordionGroup>
|
||||||
@@ -205,8 +366,76 @@ In the following steps, we explore how to use decrypt data using an existing key
|
|||||||
external sources.
|
external sources.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="What algorithms does Infisical KMS support?">
|
<Accordion title="What algorithms does Infisical KMS support?">
|
||||||
Currently, Infisical only supports `AES-128-GCM` and `AES-256-GCM` for
|
Currently Infisical supports 4 different key algorithms with different purposes:
|
||||||
encryption operations. We anticipate supporting more algorithms and
|
|
||||||
cryptographic operations in the coming months.
|
- `RSA_4096`: For signing and verifying data.
|
||||||
|
- `ECC_NIST_P256`: For signing and verifying data.
|
||||||
|
|
||||||
|
- `AES-256-GCM`: For encryption and decryption operations.
|
||||||
|
- `AES-128-GCM`: For encryption and decryption operations.
|
||||||
|
|
||||||
|
We anticipate to further expand our supported algorithms and support cryptographic operations in the future.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="How do I sign and verify a digest using the Infisical KMS?">
|
||||||
|
To sign and verify a digest using the Infisical KMS, you can use the `Sign` and `Verify` endpoints respectively.
|
||||||
|
You will need to pass `"isDigest": true` in the request body to indicate that you are signing or verifying a digest.
|
||||||
|
The data you are signing or verifying will need to be a base64 encoded digest of the data you wish to sign or verify.
|
||||||
|
It's important that the digest is created using the same hashing algorithm as the signing algorithm. As an example, you would create the digest with `SHA512` if you are using the `RSASSA_PKCS1_V1_5_SHA_512` signing algorithm.
|
||||||
|
|
||||||
|
To create a SHA512 digest of your data, you can use the following command with OpenSSL:
|
||||||
|
```bash
|
||||||
|
echo -n "Hello, World" | openssl dgst -sha512 -binary | openssl base64
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample request for signing a digest
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --request POST \
|
||||||
|
--url https://app.infisical.com/api/v1/kms/keys/<key-id>/sign \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"data": <digest-output-of-openssl-command>,
|
||||||
|
"signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512",
|
||||||
|
"isDigest": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response for signing a digest
|
||||||
|
|
||||||
|
```bash Response
|
||||||
|
{
|
||||||
|
"signature": <base64-encoded-signature>,
|
||||||
|
"keyId": <key-id>,
|
||||||
|
"signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample request for verifying a digest
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --request POST \
|
||||||
|
--url https://app.infisical.com/api/v1/kms/keys/<key-id>/verify \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"data": <digest-output-of-openssl-command>,
|
||||||
|
"signature": <base64-encoded-signature>,
|
||||||
|
"signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512",
|
||||||
|
"isDigest": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response for verifying a digest
|
||||||
|
|
||||||
|
```bash Response
|
||||||
|
{
|
||||||
|
"signatureValid": true,
|
||||||
|
"keyId": <key-id>,
|
||||||
|
"signingAlgorithm": "RSASSA_PKCS1_V1_5_SHA_512"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Please note that `RSA PSS` signing algorithms are not supported for digest signing and verification. Please use `RSA PKCS1 V1.5` signing algorithms for digest signing and verification, or `ECDSA` if you're using an ECC key.
|
||||||
|
</Note>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 595 KiB |
|
After Width: | Height: | Size: 593 KiB |
|
After Width: | Height: | Size: 596 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 195 KiB |
BIN
docs/images/platform/kms/infisical-kms/signing/sign-options.png
Normal file
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 306 KiB |
@@ -56,7 +56,15 @@ Infisical supports two methods for connecting to AWS.
|
|||||||
|
|
||||||
2. Select **AWS Account** as the **Trusted Entity Type**.
|
2. Select **AWS Account** as the **Trusted Entity Type**.
|
||||||
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
|
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
|
||||||
4. Optionally, enable **Require external ID** and enter your **Organization ID** to further enhance security.
|
4. (Recommended) <strong>Enable "Require external ID"</strong> and input your **Organization ID** to strengthen security and mitigate the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html).
|
||||||
|
|
||||||
|
<Warning type="warning" title="Security Best Practice: Use External ID to Prevent Confused Deputy Attacks">
|
||||||
|
When configuring an IAM Role that Infisical will assume, it’s highly recommended to enable the **"Require external ID"** option and specify your **Organization ID**.
|
||||||
|
|
||||||
|
This precaution helps protect your AWS account against the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html), a potential security vulnerability where Infisical could be tricked into performing actions on your behalf by an unauthorized actor.
|
||||||
|
|
||||||
|
<strong>Always enable "Require external ID" and use your Organization ID when setting up the IAM Role.</strong>
|
||||||
|
</Warning>
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Step title="Add Required Permissions to the IAM Role">
|
<Step title="Add Required Permissions to the IAM Role">
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ Infisical supports connecting to Microsoft SQL Server using database principals.
|
|||||||
- `username` - The username of the login created in the steps above
|
- `username` - The username of the login created in the steps above
|
||||||
- `password` - The password of the login created in the steps above
|
- `password` - The password of the login created in the steps above
|
||||||
- `sslCertificate` (optional) - The SSL certificate required for connection (if configured)
|
- `sslCertificate` (optional) - The SSL certificate required for connection (if configured)
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If you are self-hosting Infisical and intend to connect to an internal/private IP address, be sure to set the `ALLOW_INTERNAL_IP_CONNECTIONS` environment variable to `true`.
|
||||||
|
</Note>
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ Infisical supports connecting to PostgreSQL using a database role.
|
|||||||
- `username` - The role name of the login created in the steps above
|
- `username` - The role name of the login created in the steps above
|
||||||
- `password` - The role password of the login created in the steps above
|
- `password` - The role password of the login created in the steps above
|
||||||
- `sslCertificate` (optional) - The SSL certificate required for connection (if configured)
|
- `sslCertificate` (optional) - The SSL certificate required for connection (if configured)
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If you are self-hosting Infisical and intend to connect to an internal/private IP address, be sure to set the `ALLOW_INTERNAL_IP_CONNECTIONS` environment variable to `true`.
|
||||||
|
</Note>
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ if err != nil {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Working With Secrets
|
## Secrets
|
||||||
|
|
||||||
### List Secrets
|
### List Secrets
|
||||||
|
|
||||||
@@ -591,7 +591,7 @@ Create multiple secrets in Infisical.
|
|||||||
</Expandable>
|
</Expandable>
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
## Working With Folders
|
## Folders
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
@@ -748,3 +748,353 @@ deletedFolder, err := client.Folders().Delete(infisical.DeleteFolderOptions{
|
|||||||
</Expandable>
|
</Expandable>
|
||||||
|
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
|
## KMS
|
||||||
|
|
||||||
|
### Create Key
|
||||||
|
|
||||||
|
`client.Kms().Keys().Create(options)`
|
||||||
|
|
||||||
|
Create a new key in Infisical.
|
||||||
|
|
||||||
|
```go
|
||||||
|
newKey, err := client.Kms().Keys().Create(infisical.KmsCreateKeyOptions{
|
||||||
|
KeyUsage: "<sign-verify>|<encrypt-decrypt>",
|
||||||
|
Description: "<key-description>",
|
||||||
|
Name: "<key-name>",
|
||||||
|
EncryptionAlgorithm: "<rsa-4096>|<ecc-nist-p256>|<aes-256-gcm>|<aes-128-gcm>",
|
||||||
|
ProjectId: "<project-id>",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
<ParamField query="Parameters" type="object" optional>
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="KeyUsage" type="string" required>
|
||||||
|
The usage of the key. Valid options are `sign-verify` or `encrypt-decrypt`.
|
||||||
|
The usage dictates what the key can be used for.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Description" type="string" optional>
|
||||||
|
The description of the key.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Name" type="string" required>
|
||||||
|
The name of the key.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="EncryptionAlgorithm" type="string" required>
|
||||||
|
The encryption algorithm of the key.
|
||||||
|
|
||||||
|
Valid options for Signing/Verifying keys are:
|
||||||
|
- `rsa-4096`
|
||||||
|
- `ecc-nist-p256`
|
||||||
|
|
||||||
|
Valid options for Encryption/Decryption keys are:
|
||||||
|
- `aes-256-gcm`
|
||||||
|
- `aes-128-gcm`
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="ProjectId" type="string" required>
|
||||||
|
The ID of the project where the key will be created.
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
#### Return (object)
|
||||||
|
<ParamField query="Return" type="object">
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="KeyId" type="string" required>
|
||||||
|
The ID of the key that was created.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Name" type="string" required>
|
||||||
|
The name of the key that was created.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Description" type="string" required>
|
||||||
|
The description of the key that was created.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="IsDisabled" type="boolean" required>
|
||||||
|
Whether or not the key is disabled.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="OrgId" type="string" required>
|
||||||
|
The ID of the organization that the key belongs to.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="ProjectId" type="string" required>
|
||||||
|
The ID of the project that the key belongs to.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="KeyUsage" type="string" required>
|
||||||
|
The intended usage of the key that was created.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="EncryptionAlgorithm" type="string" required>
|
||||||
|
The encryption algorithm of the key that was created.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Version" type="string" required>
|
||||||
|
The version of the key that was created.
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
### Delete Key
|
||||||
|
|
||||||
|
`client.Kms().Keys().Delete(options)`
|
||||||
|
|
||||||
|
Delete a key in Infisical.
|
||||||
|
|
||||||
|
```go
|
||||||
|
deletedKey, err = client.Kms().Keys().Delete(infisical.KmsDeleteKeyOptions{
|
||||||
|
KeyId: "<key-id>",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
<ParamField query="Parameters" type="object" optional>
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="KeyId" type="string" required>
|
||||||
|
The ID of the key to delete.
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
#### Return (object)
|
||||||
|
<ParamField query="Return" type="object">
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="KeyId" type="string" required>
|
||||||
|
The ID of the key that was deleted
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Name" type="string" required>
|
||||||
|
The name of the key that was deleted.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Description" type="string" required>
|
||||||
|
The description of the key that was deleted.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="IsDisabled" type="boolean" required>
|
||||||
|
Whether or not the key is disabled.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="OrgId" type="string" required>
|
||||||
|
The ID of the organization that the key belonged to.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="ProjectId" type="string" required>
|
||||||
|
The ID of the project that the key belonged to.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="KeyUsage" type="string" required>
|
||||||
|
The intended usage of the key that was deleted.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="EncryptionAlgorithm" type="string" required>
|
||||||
|
The encryption algorithm of the key that was deleted.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Version" type="string" required>
|
||||||
|
The version of the key that was deleted.
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
### Signing Data
|
||||||
|
|
||||||
|
`client.Kms().Signing().Sign(options)`
|
||||||
|
Sign data in Infisical.
|
||||||
|
|
||||||
|
```go
|
||||||
|
res, err := client.Kms().Signing().SignData(infisical.KmsSignDataOptions{
|
||||||
|
KeyId: "<key-id>",
|
||||||
|
Data: "<data-to-sign>", // Must be a base64 encoded string.
|
||||||
|
SigningAlgorithm: "<signing-algorithm>", // The signing algorithm that will be used to sign the data.
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
<ParamField query="Parameters" type="object" optional>
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="KeyId" type="string" required>
|
||||||
|
The ID of the key to sign the data with.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Data" type="string" required>
|
||||||
|
The data to sign. Must be a base64 encoded string.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="IsDigest" type="boolean" optional>
|
||||||
|
Whether the data is already digested or not.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="SigningAlgorithm" type="string" required>
|
||||||
|
The signing algorithm to use. You must use a signing algorithm that matches the key usage.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If you are unsure about which signing algorithms are available for your key, you can use the `client.Kms().Signing().ListSigningAlgorithms()` method. It will return an array of signing algorithms that are available for your key.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
Valid options for `RSA 4096` keys are:
|
||||||
|
- `RSASSA_PSS_SHA_512`
|
||||||
|
- `RSASSA_PSS_SHA_384`
|
||||||
|
- `RSASSA_PSS_SHA_256`
|
||||||
|
- `RSASSA_PKCS1_V1_5_SHA_512`
|
||||||
|
- `RSASSA_PKCS1_V1_5_SHA_384`
|
||||||
|
- `RSASSA_PKCS1_V1_5_SHA_256`
|
||||||
|
|
||||||
|
Valid options for `ECC NIST P256` keys are:
|
||||||
|
- `ECDSA_SHA_512`
|
||||||
|
- `ECDSA_SHA_384`
|
||||||
|
- `ECDSA_SHA_256`
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
#### Return ([]byte)
|
||||||
|
<ParamField query="Return" type="[]byte">
|
||||||
|
The signature of the data that was signed.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
### Verifying Data
|
||||||
|
|
||||||
|
`client.Kms().Signing().Verify(options)`
|
||||||
|
Verify data in Infisical.
|
||||||
|
|
||||||
|
```go
|
||||||
|
res, err := client.Kms().Signing().Verify(infisical.KmsVerifyDataOptions{
|
||||||
|
KeyId: "<key-id>",
|
||||||
|
Data: "<data-to-verify>", // Must be a base64 encoded string.
|
||||||
|
SigningAlgorithm: "<signing-algorithm>", // The signing algorithm that was used to sign the data.
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
<ParamField query="Parameters" type="object" optional>
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="KeyId" type="string" required>
|
||||||
|
The ID of the key to verify the data with.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Data" type="string" required>
|
||||||
|
The data to verify. Must be a base64 encoded string.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="IsDigest" type="boolean" optional>
|
||||||
|
Whether the data is already digested or not.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="SigningAlgorithm" type="string" required>
|
||||||
|
The signing algorithm that was used to sign the data.
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
#### Return (object)
|
||||||
|
<ParamField query="Return" type="object">
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="SignatureValid" type="boolean" required>
|
||||||
|
Whether or not the data is valid.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="KeyId" type="string" required>
|
||||||
|
The ID of the key that was used to verify the data.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="SigningAlgorithm" type="string" required>
|
||||||
|
The signing algorithm that was used to verify the data.
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
### List Signing Algorithms
|
||||||
|
|
||||||
|
`client.Kms().Signing().ListSigningAlgorithms(options)`
|
||||||
|
List signing algorithms in Infisical.
|
||||||
|
|
||||||
|
```go
|
||||||
|
res, err := client.Kms().Signing().ListSigningAlgorithms(infisical.KmsListSigningAlgorithmsOptions{
|
||||||
|
KeyId: "<key-id>",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
<ParamField query="Parameters" type="object" optional>
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="KeyId" type="string" required>
|
||||||
|
The ID of the key to list signing algorithms for.
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
#### Return ([]string)
|
||||||
|
<ParamField query="Return" type="[]string">
|
||||||
|
The signing algorithms that are available for the key.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
### Get Public Key
|
||||||
|
<Note>
|
||||||
|
This method is only available for keys with key usage `sign-verify`. If you attempt to use this method on a key that is intended for encryption/decryption, it will return an error.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
`client.Kms().Signing().GetPublicKey(options)`
|
||||||
|
Get the public key in Infisical.
|
||||||
|
|
||||||
|
```go
|
||||||
|
publicKey, err := client.Kms().Signing().GetPublicKey(infisical.KmsGetPublicKeyOptions{
|
||||||
|
KeyId: "<key-id>",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
<ParamField query="Parameters" type="object" optional>
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="KeyId" type="string" required>
|
||||||
|
The ID of the key to get the public key for.
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
#### Return (string)
|
||||||
|
<ParamField query="Return" type="string">
|
||||||
|
The public key for the key.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
### Encrypt Data
|
||||||
|
|
||||||
|
`client.Kms().Encryption().Encrypt(options)`
|
||||||
|
Encrypt data with a key in Infisical KMS.
|
||||||
|
|
||||||
|
```go
|
||||||
|
res, err := client.Kms().EncryptData(infisical.KmsEncryptDataOptions{
|
||||||
|
KeyId: "<key-id>",
|
||||||
|
Plaintext: "<data-to-encrypt>",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
<ParamField query="Parameters" type="object" optional>
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="KeyId" type="string" required>
|
||||||
|
The ID of the key to encrypt the data with.
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
#### Return (string)
|
||||||
|
<ParamField query="Return" type="string">
|
||||||
|
The encrypted data.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
### Decrypt Data
|
||||||
|
|
||||||
|
`client.Kms().DecryptData(options)`
|
||||||
|
Decrypt data with a key in Infisical KMS.
|
||||||
|
|
||||||
|
```go
|
||||||
|
res, err := client.Kms().DecryptData(infisical.KmsDecryptDataOptions{
|
||||||
|
KeyId: "<key-id>",
|
||||||
|
Ciphertext: "<encrypted-data>",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
<ParamField query="Parameters" type="object" optional>
|
||||||
|
<Expandable title="properties">
|
||||||
|
<ParamField query="KeyId" type="string" required>
|
||||||
|
The ID of the key to decrypt the data with.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField query="Ciphertext" type="string" required>
|
||||||
|
The encrypted data to decrypt.
|
||||||
|
</ParamField>
|
||||||
|
</Expandable>
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
#### Return (string)
|
||||||
|
<ParamField query="Return" type="string">
|
||||||
|
The decrypted data.
|
||||||
|
</ParamField>
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ Used to configure platform-specific security and operational settings
|
|||||||
this to `false`.
|
this to `false`.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField query="ALLOW_INTERNAL_IP_CONNECTIONS" type="bool" default="false" optional>
|
||||||
|
Determines whether App Connections and Dynamic Secrets are permitted to connect with internal/private IP addresses.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
## CORS
|
## CORS
|
||||||
|
|
||||||
Cross-Origin Resource Sharing (CORS) is a security feature that allows web applications running on one domain to access resources from another domain.
|
Cross-Origin Resource Sharing (CORS) is a security feature that allows web applications running on one domain to access resources from another domain.
|
||||||
|
|||||||
@@ -100,7 +100,8 @@ export enum PermissionConditionOperators {
|
|||||||
$REGEX = "$regex",
|
$REGEX = "$regex",
|
||||||
$EQ = "$eq",
|
$EQ = "$eq",
|
||||||
$NEQ = "$ne",
|
$NEQ = "$ne",
|
||||||
$GLOB = "$glob"
|
$GLOB = "$glob",
|
||||||
|
$ELEMENTMATCH = "$elemMatch"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IdentityManagementSubjectFields = {
|
export type IdentityManagementSubjectFields = {
|
||||||
@@ -113,7 +114,8 @@ export const formatedConditionsOperatorNames: { [K in PermissionConditionOperato
|
|||||||
[PermissionConditionOperators.$ALL]: "contains all",
|
[PermissionConditionOperators.$ALL]: "contains all",
|
||||||
[PermissionConditionOperators.$NEQ]: "not equal to",
|
[PermissionConditionOperators.$NEQ]: "not equal to",
|
||||||
[PermissionConditionOperators.$GLOB]: "matches glob pattern",
|
[PermissionConditionOperators.$GLOB]: "matches glob pattern",
|
||||||
[PermissionConditionOperators.$REGEX]: "matches regex pattern"
|
[PermissionConditionOperators.$REGEX]: "matches regex pattern",
|
||||||
|
[PermissionConditionOperators.$ELEMENTMATCH]: "element matches"
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TPermissionConditionOperators = {
|
export type TPermissionConditionOperators = {
|
||||||
@@ -123,12 +125,24 @@ export type TPermissionConditionOperators = {
|
|||||||
[PermissionConditionOperators.$NEQ]: string;
|
[PermissionConditionOperators.$NEQ]: string;
|
||||||
[PermissionConditionOperators.$REGEX]: string;
|
[PermissionConditionOperators.$REGEX]: string;
|
||||||
[PermissionConditionOperators.$GLOB]: string;
|
[PermissionConditionOperators.$GLOB]: string;
|
||||||
|
[PermissionConditionOperators.$ELEMENTMATCH]: Record<
|
||||||
|
string,
|
||||||
|
Partial<TPermissionConditionOperators>
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TPermissionCondition = Record<
|
export type TPermissionCondition = Record<
|
||||||
string,
|
string,
|
||||||
| string
|
| string
|
||||||
| { $in: string[]; $all: string[]; $regex: string; $eq: string; $ne: string; $glob: string }
|
| {
|
||||||
|
$in: string[];
|
||||||
|
$all: string[];
|
||||||
|
$regex: string;
|
||||||
|
$eq: string;
|
||||||
|
$ne: string;
|
||||||
|
$glob: string;
|
||||||
|
$elemMatch: Partial<TPermissionCondition>;
|
||||||
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export enum ProjectPermissionSub {
|
export enum ProjectPermissionSub {
|
||||||
@@ -182,6 +196,7 @@ export type SecretFolderSubjectFields = {
|
|||||||
export type DynamicSecretSubjectFields = {
|
export type DynamicSecretSubjectFields = {
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
|
metadata?: (string | { key: string; value: string })[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SecretImportSubjectFields = {
|
export type SecretImportSubjectFields = {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type TDynamicSecret = {
|
|||||||
status?: DynamicSecretStatus;
|
status?: DynamicSecretStatus;
|
||||||
statusDetails?: string;
|
statusDetails?: string;
|
||||||
maxTTL: string;
|
maxTTL: string;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum DynamicSecretProviders {
|
export enum DynamicSecretProviders {
|
||||||
@@ -261,6 +262,7 @@ export type TDynamicSecretProvider =
|
|||||||
digits?: number;
|
digits?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateDynamicSecretDTO = {
|
export type TCreateDynamicSecretDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
provider: TDynamicSecretProvider;
|
provider: TDynamicSecretProvider;
|
||||||
@@ -269,6 +271,7 @@ export type TCreateDynamicSecretDTO = {
|
|||||||
path: string;
|
path: string;
|
||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateDynamicSecretDTO = {
|
export type TUpdateDynamicSecretDTO = {
|
||||||
@@ -278,6 +281,7 @@ export type TUpdateDynamicSecretDTO = {
|
|||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
data: {
|
data: {
|
||||||
newName?: string;
|
newName?: string;
|
||||||
|
metadata?: { key: string; value: string }[];
|
||||||
defaultTTL?: string;
|
defaultTTL?: string;
|
||||||
maxTTL?: string | null;
|
maxTTL?: string | null;
|
||||||
inputs?: unknown;
|
inputs?: unknown;
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||||
|
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
PermissionConditionOperators,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getConditionOperatorHelperInfo,
|
||||||
|
renderOperatorSelectItems
|
||||||
|
} from "./PermissionConditionHelpers";
|
||||||
|
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
position?: number;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DynamicSecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
formState: { errors }
|
||||||
|
} = useFormContext<TFormSchema>();
|
||||||
|
const items = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: `permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions`
|
||||||
|
});
|
||||||
|
|
||||||
|
const conditionErrorMessage =
|
||||||
|
errors?.permissions?.[ProjectPermissionSub.DynamicSecrets]?.[position]?.conditions?.message ||
|
||||||
|
errors?.permissions?.[ProjectPermissionSub.DynamicSecrets]?.[position]?.conditions?.root
|
||||||
|
?.message;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
||||||
|
<p className="mt-2 text-gray-300">Conditions</p>
|
||||||
|
<p className="text-sm text-mineshaft-400">
|
||||||
|
Conditions determine when a policy will be applied (always if no conditions are present).
|
||||||
|
</p>
|
||||||
|
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
||||||
|
All conditions must evaluate to true for the policy to take effect.
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 flex flex-col space-y-2">
|
||||||
|
{items.fields.map((el, index) => {
|
||||||
|
const condition = watch(
|
||||||
|
`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}`
|
||||||
|
) as {
|
||||||
|
lhs: string;
|
||||||
|
rhs: string;
|
||||||
|
operator: string;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={el.id}
|
||||||
|
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
|
||||||
|
>
|
||||||
|
<div className="w-1/4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}.lhs`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setValue(
|
||||||
|
`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}.operator`,
|
||||||
|
PermissionConditionOperators.$IN as never
|
||||||
|
);
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value="environment">Environment Slug</SelectItem>
|
||||||
|
<SelectItem value="secretPath">Secret Path</SelectItem>
|
||||||
|
<SelectItem value="metadataKey">Metadata Key</SelectItem>
|
||||||
|
<SelectItem value="metadataValue">Metadata Value</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-36 items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}.operator`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{renderOperatorSelectItems(condition.lhs)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
asChild
|
||||||
|
content={getConditionOperatorHelperInfo(
|
||||||
|
condition?.operator as PermissionConditionOperators
|
||||||
|
)}
|
||||||
|
className="max-w-xs"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}.rhs`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="plus"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="p-2.5"
|
||||||
|
onClick={() => items.remove(index)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{conditionErrorMessage && (
|
||||||
|
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||||
|
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||||
|
<span>{conditionErrorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
variant="star"
|
||||||
|
size="xs"
|
||||||
|
className="mt-3"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onClick={() =>
|
||||||
|
items.append({
|
||||||
|
lhs: "environment",
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
rhs: ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add Condition
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -285,11 +285,59 @@ const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition
|
|||||||
} else {
|
} else {
|
||||||
Object.keys(condition).forEach((conditionOperator) => {
|
Object.keys(condition).forEach((conditionOperator) => {
|
||||||
const rhs = condition[conditionOperator as PermissionConditionOperators];
|
const rhs = condition[conditionOperator as PermissionConditionOperators];
|
||||||
formConditions.push({
|
if (Array.isArray(rhs) || typeof rhs === "string") {
|
||||||
operator: conditionOperator,
|
formConditions.push({
|
||||||
lhs: type,
|
operator: conditionOperator,
|
||||||
rhs: typeof rhs === "string" ? rhs : rhs.join(",")
|
lhs: type,
|
||||||
});
|
rhs: typeof rhs === "string" ? rhs : rhs.join(",")
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
conditionOperator === PermissionConditionOperators.$ELEMENTMATCH &&
|
||||||
|
type === "metadata"
|
||||||
|
) {
|
||||||
|
const deepKeyCondition = rhs.key;
|
||||||
|
if (deepKeyCondition) {
|
||||||
|
if (typeof deepKeyCondition === "string") {
|
||||||
|
formConditions.push({
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
lhs: "metadataKey",
|
||||||
|
rhs: deepKeyCondition
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.keys(deepKeyCondition).forEach((keyOperator) => {
|
||||||
|
const deepRhs = deepKeyCondition?.[keyOperator as PermissionConditionOperators];
|
||||||
|
if (deepRhs && (Array.isArray(deepRhs) || typeof deepRhs === "string")) {
|
||||||
|
formConditions.push({
|
||||||
|
operator: keyOperator,
|
||||||
|
lhs: "metadataKey",
|
||||||
|
rhs: typeof deepRhs === "string" ? deepRhs : deepRhs.join(",")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const deepValueCondition = rhs.value;
|
||||||
|
if (deepValueCondition) {
|
||||||
|
if (typeof deepValueCondition === "string") {
|
||||||
|
formConditions.push({
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
lhs: "metadataValue",
|
||||||
|
rhs: deepValueCondition
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.keys(deepValueCondition).forEach((keyOperator) => {
|
||||||
|
const deepRhs = deepValueCondition?.[keyOperator as PermissionConditionOperators];
|
||||||
|
if (deepRhs && (Array.isArray(deepRhs) || typeof deepRhs === "string")) {
|
||||||
|
formConditions.push({
|
||||||
|
operator: keyOperator,
|
||||||
|
lhs: "metadataValue",
|
||||||
|
rhs: typeof deepRhs === "string" ? deepRhs : deepRhs.join(",")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -636,7 +684,45 @@ const convertFormOperatorToCaslCondition = (
|
|||||||
conditions: { lhs: string; rhs: string; operator: string }[]
|
conditions: { lhs: string; rhs: string; operator: string }[]
|
||||||
) => {
|
) => {
|
||||||
const caslCondition: Record<string, Partial<TPermissionConditionOperators>> = {};
|
const caslCondition: Record<string, Partial<TPermissionConditionOperators>> = {};
|
||||||
|
|
||||||
|
const metadataKeyCondition = conditions.find((condition) => condition.lhs === "metadataKey");
|
||||||
|
const metadataValueCondition = conditions.find((condition) => condition.lhs === "metadataValue");
|
||||||
|
|
||||||
|
if (metadataKeyCondition || metadataValueCondition) {
|
||||||
|
caslCondition.metadata = {
|
||||||
|
[PermissionConditionOperators.$ELEMENTMATCH]: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (metadataKeyCondition) {
|
||||||
|
const operator = metadataKeyCondition.operator as PermissionConditionOperators;
|
||||||
|
caslCondition.metadata[PermissionConditionOperators.$ELEMENTMATCH]!.key = {
|
||||||
|
[metadataKeyCondition.operator]: [
|
||||||
|
PermissionConditionOperators.$IN,
|
||||||
|
PermissionConditionOperators.$ALL
|
||||||
|
].includes(operator)
|
||||||
|
? metadataKeyCondition.rhs.split(",")
|
||||||
|
: metadataKeyCondition.rhs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadataValueCondition) {
|
||||||
|
const operator = metadataValueCondition.operator as PermissionConditionOperators;
|
||||||
|
caslCondition.metadata[PermissionConditionOperators.$ELEMENTMATCH]!.value = {
|
||||||
|
[metadataValueCondition.operator]: [
|
||||||
|
PermissionConditionOperators.$IN,
|
||||||
|
PermissionConditionOperators.$ALL
|
||||||
|
].includes(operator)
|
||||||
|
? metadataValueCondition.rhs.split(",")
|
||||||
|
: metadataValueCondition.rhs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
conditions.forEach((el) => {
|
conditions.forEach((el) => {
|
||||||
|
// these are special fields and handled above
|
||||||
|
if (el.lhs === "metadataKey" || el.lhs === "metadataValue") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!caslCondition[el.lhs]) caslCondition[el.lhs] = {};
|
if (!caslCondition[el.lhs]) caslCondition[el.lhs] = {};
|
||||||
if (
|
if (
|
||||||
el.operator === PermissionConditionOperators.$IN ||
|
el.operator === PermissionConditionOperators.$IN ||
|
||||||
@@ -647,7 +733,9 @@ const convertFormOperatorToCaslCondition = (
|
|||||||
caslCondition[el.lhs][
|
caslCondition[el.lhs][
|
||||||
el.operator as Exclude<
|
el.operator as Exclude<
|
||||||
PermissionConditionOperators,
|
PermissionConditionOperators,
|
||||||
PermissionConditionOperators.$ALL | PermissionConditionOperators.$IN
|
| PermissionConditionOperators.$ALL
|
||||||
|
| PermissionConditionOperators.$IN
|
||||||
|
| PermissionConditionOperators.$ELEMENTMATCH
|
||||||
>
|
>
|
||||||
] = el.rhs;
|
] = el.rhs;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { evaluatePermissionsAbility } from "@app/helpers/permissions";
|
|||||||
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
import { DynamicSecretPermissionConditions } from "./DynamicSecretPermissionConditions";
|
||||||
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
|
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
|
||||||
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
|
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
|
||||||
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
|
import { IdentityManagementPermissionConditions } from "./IdentityManagementPermissionConditions";
|
||||||
@@ -48,6 +49,9 @@ export const renderConditionalComponents = (
|
|||||||
if (subject === ProjectPermissionSub.Secrets)
|
if (subject === ProjectPermissionSub.Secrets)
|
||||||
return <SecretPermissionConditions isDisabled={isDisabled} />;
|
return <SecretPermissionConditions isDisabled={isDisabled} />;
|
||||||
|
|
||||||
|
if (subject === ProjectPermissionSub.DynamicSecrets)
|
||||||
|
return <DynamicSecretPermissionConditions isDisabled={isDisabled} />;
|
||||||
|
|
||||||
if (isConditionalSubjects(subject)) {
|
if (isConditionalSubjects(subject)) {
|
||||||
if (subject === ProjectPermissionSub.Identity) {
|
if (subject === ProjectPermissionSub.Identity) {
|
||||||
return <IdentityManagementPermissionConditions isDisabled={isDisabled} />;
|
return <IdentityManagementPermissionConditions isDisabled={isDisabled} />;
|
||||||
|
|||||||
@@ -207,7 +207,8 @@ export const OverviewPage = () => {
|
|||||||
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
ProjectPermissionDynamicSecretActions.CreateRootCredential,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, {
|
subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
environment: env.slug,
|
environment: env.slug,
|
||||||
secretPath
|
secretPath,
|
||||||
|
metadata: ["*"]
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ const Page = () => {
|
|||||||
|
|
||||||
const canReadDynamicSecret = permission.can(
|
const canReadDynamicSecret = permission.can(
|
||||||
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
ProjectPermissionDynamicSecretActions.ReadRootCredential,
|
||||||
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })
|
subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath, metadata: ["*"] })
|
||||||
);
|
);
|
||||||
|
|
||||||
const canReadSecretRotations = permission.can(
|
const canReadSecretRotations = permission.can(
|
||||||
@@ -532,7 +532,7 @@ const Page = () => {
|
|||||||
importedBy={importedBy}
|
importedBy={importedBy}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canReadSecret && <SecretNoAccessListView count={noAccessSecretCount} />}
|
{noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />}
|
||||||
{!canReadSecret &&
|
{!canReadSecret &&
|
||||||
!canReadDynamicSecret &&
|
!canReadDynamicSecret &&
|
||||||
!canReadSecretImports &&
|
!canReadSecretImports &&
|
||||||
|
|||||||
@@ -864,7 +864,8 @@ export const ActionBar = ({
|
|||||||
environment,
|
environment,
|
||||||
secretPath,
|
secretPath,
|
||||||
secretName: "*",
|
secretName: "*",
|
||||||
secretTags: ["*"]
|
secretTags: ["*"],
|
||||||
|
metadata: ["*"]
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
|||||||
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
|
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
import { MetadataForm } from "../../DynamicSecretListView/MetadataForm";
|
||||||
|
|
||||||
const passwordRequirementsSchema = z
|
const passwordRequirementsSchema = z
|
||||||
.object({
|
.object({
|
||||||
length: z.number().min(1).max(250),
|
length: z.number().min(1).max(250),
|
||||||
@@ -82,8 +84,16 @@ const formSchema = z.object({
|
|||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
}),
|
}),
|
||||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase"),
|
||||||
environment: z.object({ name: z.string(), slug: z.string() })
|
environment: z.object({ name: z.string(), slug: z.string() }),
|
||||||
|
metadata: z
|
||||||
|
.object({
|
||||||
|
key: z.string().trim().min(1),
|
||||||
|
value: z.string().trim().default("")
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type TForm = z.infer<typeof formSchema>;
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -192,7 +202,8 @@ export const SqlDatabaseInputForm = ({
|
|||||||
maxTTL,
|
maxTTL,
|
||||||
provider,
|
provider,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
environment
|
environment,
|
||||||
|
metadata
|
||||||
}: TForm) => {
|
}: TForm) => {
|
||||||
// wait till previous request is finished
|
// wait till previous request is finished
|
||||||
if (createDynamicSecret.isPending) return;
|
if (createDynamicSecret.isPending) return;
|
||||||
@@ -205,7 +216,8 @@ export const SqlDatabaseInputForm = ({
|
|||||||
path: secretPath,
|
path: secretPath,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
projectSlug,
|
projectSlug,
|
||||||
environmentSlug: environment.slug
|
environmentSlug: environment.slug,
|
||||||
|
metadata
|
||||||
});
|
});
|
||||||
onCompleted();
|
onCompleted();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -283,46 +295,47 @@ export const SqlDatabaseInputForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<MetadataForm control={control} />
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="provider.projectGatewayId"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
errorText={error?.message}
|
|
||||||
label="Gateway"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
value={value}
|
|
||||||
onValueChange={onChange}
|
|
||||||
className="w-full border border-mineshaft-500"
|
|
||||||
dropdownContainerClassName="max-w-none"
|
|
||||||
isLoading={isProjectGatewaysLoading}
|
|
||||||
placeholder="Internet gateway"
|
|
||||||
position="popper"
|
|
||||||
>
|
|
||||||
<SelectItem
|
|
||||||
value={null as unknown as string}
|
|
||||||
onClick={() => onChange(undefined)}
|
|
||||||
>
|
|
||||||
Internet Gateway
|
|
||||||
</SelectItem>
|
|
||||||
{projectGateways?.map((el) => (
|
|
||||||
<SelectItem value={el.projectGatewayId} key={el.projectGatewayId}>
|
|
||||||
{el.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||||
Configuration
|
Configuration
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="provider.projectGatewayId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Gateway"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
isLoading={isProjectGatewaysLoading}
|
||||||
|
placeholder="Internet gateway"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={null as unknown as string}
|
||||||
|
onClick={() => onChange(undefined)}
|
||||||
|
>
|
||||||
|
Internet Gateway
|
||||||
|
</SelectItem>
|
||||||
|
{projectGateways?.map((el) => (
|
||||||
|
<SelectItem value={el.projectGatewayId} key={el.projectGatewayId}>
|
||||||
|
{el.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="pb-0.5 pl-1 text-sm text-mineshaft-400">Service</div>
|
<div className="pb-0.5 pl-1 text-sm text-mineshaft-400">Service</div>
|
||||||
<Controller
|
<Controller
|
||||||
|
|||||||
@@ -29,11 +29,13 @@ import {
|
|||||||
import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionDynamicSecretActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useGetDynamicSecretLeases, useRevokeDynamicSecretLease } from "@app/hooks/api";
|
import { useGetDynamicSecretLeases, useRevokeDynamicSecretLease } from "@app/hooks/api";
|
||||||
|
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||||
import { DynamicSecretLeaseStatus } from "@app/hooks/api/dynamicSecretLease/types";
|
import { DynamicSecretLeaseStatus } from "@app/hooks/api/dynamicSecretLease/types";
|
||||||
|
|
||||||
import { RenewDynamicSecretLease } from "./RenewDynamicSecretLease";
|
import { RenewDynamicSecretLease } from "./RenewDynamicSecretLease";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
dynamicSecret: TDynamicSecret;
|
||||||
dynamicSecretName: string;
|
dynamicSecretName: string;
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
@@ -48,7 +50,8 @@ export const DynamicSecretLease = ({
|
|||||||
environment,
|
environment,
|
||||||
secretPath,
|
secretPath,
|
||||||
onClickNewLease,
|
onClickNewLease,
|
||||||
onClose
|
onClose,
|
||||||
|
dynamicSecret
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { handlePopUpOpen, popUp, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { handlePopUpOpen, popUp, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"deleteSecret",
|
"deleteSecret",
|
||||||
@@ -140,7 +143,11 @@ export const DynamicSecretLease = ({
|
|||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionDynamicSecretActions.Lease}
|
I={ProjectPermissionDynamicSecretActions.Lease}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
metadata: dynamicSecret.metadata
|
||||||
|
})}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Renew"
|
allowedLabel="Renew"
|
||||||
>
|
>
|
||||||
@@ -159,7 +166,11 @@ export const DynamicSecretLease = ({
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionDynamicSecretActions.Lease}
|
I={ProjectPermissionDynamicSecretActions.Lease}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
metadata: dynamicSecret.metadata
|
||||||
|
})}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Delete"
|
allowedLabel="Delete"
|
||||||
>
|
>
|
||||||
@@ -181,7 +192,8 @@ export const DynamicSecretLease = ({
|
|||||||
I={ProjectPermissionDynamicSecretActions.Lease}
|
I={ProjectPermissionDynamicSecretActions.Lease}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
environment,
|
environment,
|
||||||
secretPath
|
secretPath,
|
||||||
|
metadata: dynamicSecret.metadata
|
||||||
})}
|
})}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Force Delete. This action will remove the secret from internal storage, but it will remain in external systems."
|
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}
|
I={ProjectPermissionDynamicSecretActions.Lease}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
environment,
|
environment,
|
||||||
secretPath
|
secretPath,
|
||||||
|
metadata: dynamicSecret.metadata
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
|
|||||||
@@ -144,7 +144,11 @@ export const DynamicSecretListView = ({
|
|||||||
<div className="flex items-center space-x-2 px-4 py-2">
|
<div className="flex items-center space-x-2 px-4 py-2">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionDynamicSecretActions.Lease}
|
I={ProjectPermissionDynamicSecretActions.Lease}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
metadata: secret.metadata
|
||||||
|
})}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Edit"
|
allowedLabel="Edit"
|
||||||
>
|
>
|
||||||
@@ -186,7 +190,11 @@ export const DynamicSecretListView = ({
|
|||||||
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionDynamicSecretActions.EditRootCredential}
|
I={ProjectPermissionDynamicSecretActions.EditRootCredential}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
metadata: secret.metadata
|
||||||
|
})}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Edit"
|
allowedLabel="Edit"
|
||||||
>
|
>
|
||||||
@@ -208,7 +216,11 @@ export const DynamicSecretListView = ({
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionDynamicSecretActions.DeleteRootCredential}
|
I={ProjectPermissionDynamicSecretActions.DeleteRootCredential}
|
||||||
a={subject(ProjectPermissionSub.DynamicSecrets, { environment, secretPath })}
|
a={subject(ProjectPermissionSub.DynamicSecrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
metadata: secret.metadata
|
||||||
|
})}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Delete"
|
allowedLabel="Delete"
|
||||||
>
|
>
|
||||||
@@ -236,6 +248,7 @@ export const DynamicSecretListView = ({
|
|||||||
className="max-w-3xl"
|
className="max-w-3xl"
|
||||||
>
|
>
|
||||||
<DynamicSecretLease
|
<DynamicSecretLease
|
||||||
|
dynamicSecret={secret}
|
||||||
onClickNewLease={() => handlePopUpOpen("createDynamicSecretLease", secret)}
|
onClickNewLease={() => handlePopUpOpen("createDynamicSecretLease", secret)}
|
||||||
onClose={() => handlePopUpClose("dynamicSecretLeases")}
|
onClose={() => handlePopUpClose("dynamicSecretLeases")}
|
||||||
projectSlug={projectSlug}
|
projectSlug={projectSlug}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { useWorkspace } from "@app/context";
|
|||||||
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
|
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
|
||||||
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||||
|
|
||||||
|
import { MetadataForm } from "../MetadataForm";
|
||||||
|
|
||||||
const passwordRequirementsSchema = z
|
const passwordRequirementsSchema = z
|
||||||
.object({
|
.object({
|
||||||
length: z.number().min(1).max(250),
|
length: z.number().min(1).max(250),
|
||||||
@@ -85,6 +87,13 @@ const formSchema = z.object({
|
|||||||
newName: z
|
newName: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||||
|
.optional(),
|
||||||
|
metadata: z
|
||||||
|
.object({
|
||||||
|
key: z.string().trim().min(1),
|
||||||
|
value: z.string().trim().default("")
|
||||||
|
})
|
||||||
|
.array()
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
type TForm = z.infer<typeof formSchema>;
|
type TForm = z.infer<typeof formSchema>;
|
||||||
@@ -126,6 +135,7 @@ export const EditDynamicSecretSqlProviderForm = ({
|
|||||||
defaultTTL: dynamicSecret.defaultTTL,
|
defaultTTL: dynamicSecret.defaultTTL,
|
||||||
maxTTL: dynamicSecret.maxTTL,
|
maxTTL: dynamicSecret.maxTTL,
|
||||||
newName: dynamicSecret.name,
|
newName: dynamicSecret.name,
|
||||||
|
metadata: dynamicSecret.metadata,
|
||||||
inputs: {
|
inputs: {
|
||||||
...(dynamicSecret.inputs as TForm["inputs"]),
|
...(dynamicSecret.inputs as TForm["inputs"]),
|
||||||
passwordRequirements:
|
passwordRequirements:
|
||||||
@@ -147,7 +157,13 @@ export const EditDynamicSecretSqlProviderForm = ({
|
|||||||
const isGatewayInActive =
|
const isGatewayInActive =
|
||||||
projectGateways?.findIndex((el) => el.projectGatewayId === selectedProjectGatewayId) === -1;
|
projectGateways?.findIndex((el) => el.projectGatewayId === selectedProjectGatewayId) === -1;
|
||||||
|
|
||||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
const handleUpdateDynamicSecret = async ({
|
||||||
|
inputs,
|
||||||
|
maxTTL,
|
||||||
|
defaultTTL,
|
||||||
|
newName,
|
||||||
|
metadata
|
||||||
|
}: TForm) => {
|
||||||
// wait till previous request is finished
|
// wait till previous request is finished
|
||||||
if (updateDynamicSecret.isPending) return;
|
if (updateDynamicSecret.isPending) return;
|
||||||
try {
|
try {
|
||||||
@@ -163,7 +179,8 @@ export const EditDynamicSecretSqlProviderForm = ({
|
|||||||
...inputs,
|
...inputs,
|
||||||
projectGatewayId: isGatewayInActive ? null : inputs.projectGatewayId
|
projectGatewayId: isGatewayInActive ? null : inputs.projectGatewayId
|
||||||
},
|
},
|
||||||
newName: newName === dynamicSecret.name ? undefined : newName
|
newName: newName === dynamicSecret.name ? undefined : newName,
|
||||||
|
metadata
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
@@ -229,46 +246,50 @@ export const EditDynamicSecretSqlProviderForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<MetadataForm control={control} />
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="inputs.projectGatewayId"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error?.message) || isGatewayInActive}
|
|
||||||
errorText={
|
|
||||||
isGatewayInActive && selectedProjectGatewayId
|
|
||||||
? `Project Gateway ${selectedProjectGatewayId} is removed`
|
|
||||||
: error?.message
|
|
||||||
}
|
|
||||||
label="Gateway"
|
|
||||||
helperText=""
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
value={value || undefined}
|
|
||||||
onValueChange={onChange}
|
|
||||||
className="w-full border border-mineshaft-500"
|
|
||||||
dropdownContainerClassName="max-w-none"
|
|
||||||
isLoading={isProjectGatewaysLoading}
|
|
||||||
placeholder="Internet Gateway"
|
|
||||||
position="popper"
|
|
||||||
>
|
|
||||||
<SelectItem value={null as unknown as string} onClick={() => onChange(undefined)}>
|
|
||||||
Internet Gateway
|
|
||||||
</SelectItem>
|
|
||||||
{projectGateways?.map((el) => (
|
|
||||||
<SelectItem value={el.projectGatewayId} key={el.id}>
|
|
||||||
{el.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
|
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="inputs.projectGatewayId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message) || isGatewayInActive}
|
||||||
|
errorText={
|
||||||
|
isGatewayInActive && selectedProjectGatewayId
|
||||||
|
? `Project Gateway ${selectedProjectGatewayId} is removed`
|
||||||
|
: error?.message
|
||||||
|
}
|
||||||
|
label="Gateway"
|
||||||
|
helperText=""
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={value || undefined}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
isLoading={isProjectGatewaysLoading}
|
||||||
|
placeholder="Internet Gateway"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={null as unknown as string}
|
||||||
|
onClick={() => onChange(undefined)}
|
||||||
|
>
|
||||||
|
Internet Gateway
|
||||||
|
</SelectItem>
|
||||||
|
{projectGateways?.map((el) => (
|
||||||
|
<SelectItem value={el.projectGatewayId} key={el.id}>
|
||||||
|
{el.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Control, Controller, useFieldArray } from "react-hook-form";
|
||||||
|
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { FormControl, FormLabel, IconButton, Input } from "@app/components/v2";
|
||||||
|
|
||||||
|
export const MetadataForm = ({ control }: { control: Control<any> }) => {
|
||||||
|
const metadataFormFields = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "metadata"
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl label="Metadata">
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
|
||||||
|
<div key={metadataFieldId} className="flex items-end space-x-2">
|
||||||
|
<div className="flex-grow">
|
||||||
|
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`metadata.${i}.key`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Input {...field} className="max-h-8" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
{i === 0 && (
|
||||||
|
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`metadata.${i}.value`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Input {...field} className="max-h-8" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete key"
|
||||||
|
className="bottom-0.5 max-h-8"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => metadataFormFields.remove(i)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className={`${metadataFormFields.fields.length > 0 ? "pt-2" : ""}`}>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="Add Key"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
className="rounded-md"
|
||||||
|
onClick={() => metadataFormFields.append({ key: "", value: "" })}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||