feat: secret approval and replication

This commit is contained in:
Sheen Capadngan
2024-12-21 02:51:47 +08:00
parent ae07d38c19
commit 1a68765f15
11 changed files with 153 additions and 16 deletions
@@ -19,8 +19,22 @@ export async function up(knex: Knex): Promise<void> {
tb.timestamps(true, true, true); tb.timestamps(true, true, true);
}); });
} }
const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata");
if (!hasSecretMetadataField) {
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
t.jsonb("secretMetadata");
});
}
} }
export async function down(knex: Knex): Promise<void> { export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.ResourceMetadata); await knex.schema.dropTableIfExists(TableName.ResourceMetadata);
const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata");
if (hasSecretMetadataField) {
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
t.dropColumn("secretMetadata");
});
}
} }
@@ -24,7 +24,8 @@ export const SecretApprovalRequestsSecretsV2Schema = z.object({
requestId: z.string().uuid(), requestId: z.string().uuid(),
op: z.string(), op: z.string(),
secretId: z.string().uuid().nullable().optional(), secretId: z.string().uuid().nullable().optional(),
secretVersion: z.string().uuid().nullable().optional() secretVersion: z.string().uuid().nullable().optional(),
secretMetadata: z.unknown().nullable().optional()
}); });
export type TSecretApprovalRequestsSecretsV2 = z.infer<typeof SecretApprovalRequestsSecretsV2Schema>; export type TSecretApprovalRequestsSecretsV2 = z.infer<typeof SecretApprovalRequestsSecretsV2Schema>;
@@ -12,6 +12,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { secretRawSchema } 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";
const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge( const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge(
UsersSchema.pick({ UsersSchema.pick({
@@ -274,6 +275,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
.extend({ .extend({
op: z.string(), op: z.string(),
tags: tagSchema, tags: tagSchema,
secretMetadata: ResourceMetadataSchema.nullish(),
secret: z secret: z
.object({ .object({
id: z.string(), id: z.string(),
@@ -291,7 +293,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
secretKey: z.string(), secretKey: z.string(),
secretValue: z.string().optional(), secretValue: z.string().optional(),
secretComment: z.string().optional(), secretComment: z.string().optional(),
tags: tagSchema tags: tagSchema,
secretMetadata: ResourceMetadataSchema.nullish()
}) })
.optional() .optional()
}) })
@@ -256,6 +256,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`, `${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
db.ref("id").withSchema("secVerTag") db.ref("id").withSchema("secVerTag")
) )
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2)) .select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
.select({ .select({
secVerTagId: "secVerTag.id", secVerTagId: "secVerTag.id",
@@ -279,6 +280,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"), db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"),
db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"), db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"),
db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment") db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment")
)
.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")
); );
const formatedDoc = sqlNestRelationships({ const formatedDoc = sqlNestRelationships({
data: doc, data: doc,
@@ -338,9 +344,19 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
}) })
} }
] ]
},
{
key: "metadataId",
label: "oldSecretMetadata" as const,
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
id: metadataId,
key: metadataKey,
value: metadataValue
})
} }
] ]
}); });
return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({ return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({
...el, ...el,
secret: secret?.[0], secret: secret?.[0],
@@ -23,6 +23,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { import {
decryptSecretWithBot, decryptSecretWithBot,
@@ -244,6 +245,7 @@ export const secretApprovalRequestServiceFactory = ({
secretKey: el.key, secretKey: el.key,
id: el.id, id: el.id,
version: el.version, version: el.version,
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
secretComment: el.encryptedComment secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
@@ -272,7 +274,8 @@ export const secretApprovalRequestServiceFactory = ({
secretComment: el.secretVersion.encryptedComment secretComment: el.secretVersion.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
: "", : "",
tags: el.secretVersion.tags tags: el.secretVersion.tags,
secretMetadata: el.oldSecretMetadata as ResourceMetadataDTO
} }
: undefined : undefined
})); }));
@@ -554,6 +557,7 @@ export const secretApprovalRequestServiceFactory = ({
encryptedValue: el.encryptedValue, encryptedValue: el.encryptedValue,
skipMultilineEncoding: el.skipMultilineEncoding, skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key, key: el.key,
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
references: el.encryptedValue references: el.encryptedValue
? getAllSecretReferencesV2Bridge( ? getAllSecretReferencesV2Bridge(
secretManagerDecryptor({ secretManagerDecryptor({
@@ -598,6 +602,7 @@ export const secretApprovalRequestServiceFactory = ({
skipMultilineEncoding: el.skipMultilineEncoding, skipMultilineEncoding: el.skipMultilineEncoding,
key: el.key, key: el.key,
tags: el?.tags.map(({ id }) => id), tags: el?.tags.map(({ id }) => id),
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
...encryptedValue ...encryptedValue
} }
}; };
@@ -1216,6 +1221,7 @@ export const secretApprovalRequestServiceFactory = ({
), ),
skipMultilineEncoding: createdSecret.skipMultilineEncoding, skipMultilineEncoding: createdSecret.skipMultilineEncoding,
key: createdSecret.secretKey, key: createdSecret.secretKey,
secretMetadata: createdSecret.secretMetadata,
type: SecretType.Shared type: SecretType.Shared
})) }))
); );
@@ -1271,12 +1277,14 @@ export const secretApprovalRequestServiceFactory = ({
reminderNote, reminderNote,
secretComment, secretComment,
metadata, metadata,
skipMultilineEncoding skipMultilineEncoding,
secretMetadata
}) => { }) => {
const secretId = updatingSecretsGroupByKey[secretKey][0].id; const secretId = updatingSecretsGroupByKey[secretKey][0].id;
if (tagIds?.length) commitTagIds[secretKey] = tagIds; if (tagIds?.length) commitTagIds[secretKey] = tagIds;
return { return {
...latestSecretVersions[secretId], ...latestSecretVersions[secretId],
secretMetadata,
key: newSecretName || secretKey, key: newSecretName || secretKey,
encryptedComment: setKnexStringValue( encryptedComment: setKnexStringValue(
secretComment, secretComment,
@@ -1378,7 +1386,8 @@ export const secretApprovalRequestServiceFactory = ({
reminderRepeatDays, reminderRepeatDays,
encryptedValue, encryptedValue,
secretId, secretId,
secretVersion secretVersion,
secretMetadata
}) => ({ }) => ({
version, version,
requestId: doc.id, requestId: doc.id,
@@ -1391,7 +1400,8 @@ export const secretApprovalRequestServiceFactory = ({
reminderRepeatDays, reminderRepeatDays,
reminderNote, reminderNote,
encryptedComment, encryptedComment,
key key,
secretMetadata: JSON.stringify(secretMetadata)
}) })
), ),
tx tx
@@ -1,5 +1,6 @@
import { TImmutableDBKeys, TSecretApprovalPolicies, TSecretApprovalRequestsSecrets } from "@app/db/schemas"; import { TImmutableDBKeys, TSecretApprovalPolicies, TSecretApprovalRequestsSecrets } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretOperations } from "@app/services/secret/secret-types"; import { SecretOperations } from "@app/services/secret/secret-types";
export enum RequestState { export enum RequestState {
@@ -34,6 +35,7 @@ export type TApprovalCreateSecretV2Bridge = {
reminderRepeatDays?: number | null; reminderRepeatDays?: number | null;
skipMultilineEncoding?: boolean; skipMultilineEncoding?: boolean;
metadata?: Record<string, string>; metadata?: Record<string, string>;
secretMetadata?: ResourceMetadataDTO;
tagIds?: string[]; tagIds?: string[];
}; };
@@ -396,6 +396,7 @@ export const secretReplicationServiceFactory = ({
op: operation, op: operation,
requestId: approvalRequestDoc.id, requestId: approvalRequestDoc.id,
metadata: doc.metadata, metadata: doc.metadata,
secretMetadata: JSON.stringify(doc.secretMetadata),
key: doc.key, key: doc.key,
encryptedValue: doc.encryptedValue, encryptedValue: doc.encryptedValue,
encryptedComment: doc.encryptedComment, encryptedComment: doc.encryptedComment,
@@ -1419,7 +1419,8 @@ export const secretServiceFactory = ({
secretValue, secretValue,
tagIds, tagIds,
reminderNote: secretReminderNote, reminderNote: secretReminderNote,
reminderRepeatDays: secretReminderRepeatDays reminderRepeatDays: secretReminderRepeatDays,
secretMetadata
} }
] ]
} }
@@ -1562,7 +1563,8 @@ export const secretServiceFactory = ({
secretValue, secretValue,
tagIds, tagIds,
reminderNote: secretReminderNote, reminderNote: secretReminderNote,
reminderRepeatDays: secretReminderRepeatDays reminderRepeatDays: secretReminderRepeatDays,
secretMetadata
} }
] ]
} }
@@ -1803,7 +1805,8 @@ export const secretServiceFactory = ({
secretComment: el.secretComment, secretComment: el.secretComment,
metadata: el.metadata, metadata: el.metadata,
skipMultilineEncoding: el.skipMultilineEncoding, skipMultilineEncoding: el.skipMultilineEncoding,
secretKey: el.secretKey secretKey: el.secretKey,
secretMetadata: el.secretMetadata
})) }))
} }
}); });
@@ -252,6 +252,7 @@ export type TCreateManySecretRawDTO = Omit<TProjectPermission, "projectId"> & {
secretComment?: string; secretComment?: string;
skipMultilineEncoding?: boolean; skipMultilineEncoding?: boolean;
tagIds?: string[]; tagIds?: string[];
secretMetadata?: ResourceMetadataDTO;
metadata?: { metadata?: {
source?: string; source?: string;
}; };
@@ -82,6 +82,7 @@ export type TSecretApprovalRequest = {
conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>; conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>;
commits: ({ commits: ({
// if there is no secret means it was creation // if there is no secret means it was creation
secretMetadata?: { key: string; value: string }[];
secret?: { version: number }; secret?: { version: number };
secretVersion: SecretV3Raw; secretVersion: SecretV3Raw;
// if there is no new version its for Delete // if there is no new version its for Delete
@@ -1,4 +1,4 @@
import { faExclamationTriangle, faInfo } from "@fortawesome/free-solid-svg-icons"; import { faExclamationTriangle, faInfo, faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
@@ -18,7 +18,10 @@ import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/h
export type Props = { export type Props = {
op: CommitType; op: CommitType;
secretVersion?: SecretV3Raw; secretVersion?: SecretV3Raw;
newVersion?: Omit<TSecretApprovalSecChange, "tags"> & { tags?: WsTag[] }; newVersion?: Omit<TSecretApprovalSecChange, "tags"> & {
tags?: WsTag[];
secretMetadata?: { key: string; value: string }[];
};
presentSecretVersionNumber: number; presentSecretVersionNumber: number;
hasMerged?: Boolean; hasMerged?: Boolean;
conflicts: Array<{ secretId: string; op: CommitType }>; conflicts: Array<{ secretId: string; op: CommitType }>;
@@ -80,11 +83,12 @@ export const SecretApprovalRequestChangeItem = ({
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
{op === CommitType.UPDATE && <Th className="w-12" />} {op === CommitType.UPDATE && <Th className="w-12 shrink-0" />}
<Th className="min-table-row">Secret</Th> <Th className="w-48 shrink-0">Secret</Th>
<Th>Value</Th> <Th className="min-w-0 flex-1">Value</Th>
<Th className="min-table-row">Comment</Th> <Th className="w-24 shrink-0">Comment</Th>
<Th className="min-table-row">Tags</Th> <Th className="w-24 shrink-0">Tags</Th>
<Th className="w-40 shrink-0">Metadata</Th>
</Tr> </Tr>
</THead> </THead>
{op === CommitType.UPDATE ? ( {op === CommitType.UPDATE ? (
@@ -110,6 +114,33 @@ export const SecretApprovalRequestChangeItem = ({
</Tag> </Tag>
))} ))}
</Td> </Td>
<Td>
{secretVersion?.secretMetadata?.length ? (
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
{secretVersion.secretMetadata?.map((el) => (
<div key={el.key} className="flex items-center">
<Tag
size="xs"
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
>
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
<div>{el.key}</div>
</Tag>
<Tag
size="xs"
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
>
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
</Tag>
</div>
))}
</div>
) : (
<p className="text-sm text-mineshaft-300">-</p>
)}
</Td>
</Tr> </Tr>
<Tr> <Tr>
<Td className="text-green-600">NEW</Td> <Td className="text-green-600">NEW</Td>
@@ -132,6 +163,33 @@ export const SecretApprovalRequestChangeItem = ({
</Tag> </Tag>
))} ))}
</Td> </Td>
<Td>
{newVersion?.secretMetadata?.length ? (
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
{newVersion.secretMetadata?.map((el) => (
<div key={el.key} className="flex items-center">
<Tag
size="xs"
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
>
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
<div>{el.key}</div>
</Tag>
<Tag
size="xs"
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
>
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
</Tag>
</div>
))}
</div>
) : (
<p className="text-sm text-mineshaft-300">-</p>
)}
</Td>
</Tr> </Tr>
</TBody> </TBody>
) : ( ) : (
@@ -173,6 +231,33 @@ export const SecretApprovalRequestChangeItem = ({
) )
)} )}
</Td> </Td>
<Td>
{newVersion?.secretMetadata?.length ? (
<div className="mt-1 flex flex-wrap gap-2 text-sm text-mineshaft-300">
{newVersion.secretMetadata?.map((el) => (
<div key={el.key} className="flex items-center">
<Tag
size="xs"
className="mr-0 flex items-center rounded-r-none border border-mineshaft-500"
>
<FontAwesomeIcon icon={faKey} size="xs" className="mr-1" />
<div>{el.key}</div>
</Tag>
<Tag
size="xs"
className="flex items-center rounded-l-none border border-mineshaft-500 bg-mineshaft-900 pl-1"
>
<div className="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap">
{el.value}
</div>
</Tag>
</div>
))}
</div>
) : (
<p className="text-sm text-mineshaft-300">-</p>
)}
</Td>
</Tr> </Tr>
</TBody> </TBody>
)} )}