From 345edb3f15abc6f738119a97f4dbed74081b41ba Mon Sep 17 00:00:00 2001 From: = Date: Fri, 14 Jun 2024 18:31:54 +0530 Subject: [PATCH] feat: allow secret tag api for machine identity and raw secret endpoint tag support --- .../20240614115952_tag-machine-identity.ts | 25 +++++++++++++ backend/src/db/schemas/secret-tags.ts | 3 +- backend/src/lib/api-docs/constants.ts | 6 ++-- .../src/server/routes/v1/secret-tag-router.ts | 6 ++-- backend/src/server/routes/v3/secret-router.ts | 21 ++++++++--- .../services/secret-tag/secret-tag-service.ts | 3 +- backend/src/services/secret/secret-dal.ts | 35 +++++++++++++++++++ backend/src/services/secret/secret-fns.ts | 13 ++++++- backend/src/services/secret/secret-service.ts | 14 +++++--- backend/src/services/secret/secret-types.ts | 2 ++ 10 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 backend/src/db/migrations/20240614115952_tag-machine-identity.ts diff --git a/backend/src/db/migrations/20240614115952_tag-machine-identity.ts b/backend/src/db/migrations/20240614115952_tag-machine-identity.ts new file mode 100644 index 000000000..fd11928b6 --- /dev/null +++ b/backend/src/db/migrations/20240614115952_tag-machine-identity.ts @@ -0,0 +1,25 @@ +import { Knex } from "knex"; + +import { ActorType } from "@app/services/auth/auth-type"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasCreatedByActorType = await knex.schema.hasColumn(TableName.SecretTag, "createdByActorType"); + await knex.schema.alterTable(TableName.SecretTag, (tb) => { + if (!hasCreatedByActorType) { + tb.string("createdByActorType").notNullable().defaultTo(ActorType.USER); + tb.dropForeign("createdBy"); + } + }); +} + +export async function down(knex: Knex): Promise { + const hasCreatedByActorType = await knex.schema.hasColumn(TableName.SecretTag, "createdByActorType"); + await knex.schema.alterTable(TableName.SecretTag, (tb) => { + if (hasCreatedByActorType) { + tb.dropColumn("createdByActorType"); + tb.foreign("createdBy").references("id").inTable(TableName.Users).onDelete("SET NULL"); + } + }); +} diff --git a/backend/src/db/schemas/secret-tags.ts b/backend/src/db/schemas/secret-tags.ts index f94e1e262..04bb7b752 100644 --- a/backend/src/db/schemas/secret-tags.ts +++ b/backend/src/db/schemas/secret-tags.ts @@ -15,7 +15,8 @@ export const SecretTagsSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), createdBy: z.string().uuid().nullable().optional(), - projectId: z.string() + projectId: z.string(), + createdByActorType: z.string().default("user") }); export type TSecretTags = z.infer; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 1637b266a..5c67c489e 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -343,7 +343,8 @@ export const RAW_SECRETS = { secretValue: "The value of the secret to create.", skipMultilineEncoding: "Skip multiline encoding for the secret value.", type: "The type of the secret to create.", - workspaceId: "The ID of the project to create the secret in." + workspaceId: "The ID of the project to create the secret in.", + tagIds: "The ID of the tags to be attached to the created secret." }, GET: { secretName: "The name of the secret to get.", @@ -364,7 +365,8 @@ export const RAW_SECRETS = { skipMultilineEncoding: "Skip multiline encoding for the secret value.", type: "The type of the secret to update.", projectSlug: "The slug of the project to update the secret in.", - workspaceId: "The ID of the project to update the secret in." + workspaceId: "The ID of the project to update the secret in.", + tagIds: "The ID of the tags to be attached to the updated secret." }, DELETE: { secretName: "The name of the secret to delete.", diff --git a/backend/src/server/routes/v1/secret-tag-router.ts b/backend/src/server/routes/v1/secret-tag-router.ts index 1715aa3c3..ccbb4572d 100644 --- a/backend/src/server/routes/v1/secret-tag-router.ts +++ b/backend/src/server/routes/v1/secret-tag-router.ts @@ -23,7 +23,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const workspaceTags = await server.services.secretTag.getProjectTags({ actor: req.permission.type, @@ -57,7 +57,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const workspaceTag = await server.services.secretTag.createTag({ actor: req.permission.type, @@ -88,7 +88,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const workspaceTag = await server.services.secretTag.deleteTag({ actor: req.permission.type, diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 05db617b9..d620e1fac 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -306,7 +306,16 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - secret: secretRawSchema + secret: secretRawSchema.extend({ + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + name: true, + color: true + }) + .array() + .optional() + }) }) } }, @@ -404,6 +413,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) .describe(RAW_SECRETS.CREATE.secretValue), secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment), + tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds), skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding), type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type) }), @@ -427,7 +437,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { type: req.body.type, secretValue: req.body.secretValue, skipMultilineEncoding: req.body.skipMultilineEncoding, - secretComment: req.body.secretComment + secretComment: req.body.secretComment, + tagIds: req.body.tagIds }); await server.services.auditLog.createAuditLog({ @@ -492,7 +503,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .transform(removeTrailingSlash) .describe(RAW_SECRETS.UPDATE.secretPath), skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding), - type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type) + type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type), + tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds) }), response: { 200: z.object({ @@ -513,7 +525,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretName: req.params.secretName, type: req.body.type, secretValue: req.body.secretValue, - skipMultilineEncoding: req.body.skipMultilineEncoding + skipMultilineEncoding: req.body.skipMultilineEncoding, + tagIds: req.body.tagIds }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/services/secret-tag/secret-tag-service.ts b/backend/src/services/secret-tag/secret-tag-service.ts index ed8f5fec7..916e812e6 100644 --- a/backend/src/services/secret-tag/secret-tag-service.ts +++ b/backend/src/services/secret-tag/secret-tag-service.ts @@ -42,7 +42,8 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe name, slug, color, - createdBy: actorId + createdBy: actorId, + createdByActorType: actor }); return newTag; }; diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index 1a2e414dd..790b403dd 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -311,6 +311,40 @@ export const secretDALFactory = (db: TDbClient) => { } }; + const findOneWithTags = async (filter: Partial, tx?: Knex) => { + try { + const rawDocs = await (tx || db)(TableName.Secret) + .where(filter) + .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) + .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) + .select(selectAllTableCols(TableName.Secret)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) + .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName")); + const docs = sqlNestRelationships({ + data: rawDocs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ + id, + color, + slug, + name + }) + } + ] + }); + return docs?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: "FindOneWIthTags" }); + } + }; + return { ...secretOrm, update, @@ -318,6 +352,7 @@ export const secretDALFactory = (db: TDbClient) => { deleteMany, bulkUpdateNoVersionIncrement, getSecretTags, + findOneWithTags, findByFolderId, findByFolderIds, findByBlindIndexes, diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 6758f4815..aa112e6b0 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -356,7 +356,17 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD }; export const decryptSecretRaw = ( - secret: TSecrets & { workspace: string; environment: string; secretPath: string }, + secret: TSecrets & { + workspace: string; + environment: string; + secretPath: string; + tags?: { + id: string; + slug: string; + color?: string | null; + name: string; + }[]; + }, key: string ) => { const secretKey = decryptSymmetric128BitHexKeyUTF8({ @@ -396,6 +406,7 @@ export const decryptSecretRaw = ( _id: secret.id, id: secret.id, user: secret.userId, + tags: secret.tags, skipMultilineEncoding: secret.skipMultilineEncoding }; }; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index d6682a253..a5a469a8f 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -608,7 +608,7 @@ export const secretServiceFactory = ({ } const secret = await (version === undefined - ? secretDAL.findOne({ + ? secretDAL.findOneWithTags({ folderId, type: secretType, userId: secretType === SecretType.Personal ? actorId : null, @@ -1120,7 +1120,8 @@ export const secretServiceFactory = ({ secretPath, secretValue, secretComment, - skipMultilineEncoding + skipMultilineEncoding, + tagIds }: TCreateSecretRawDTO) => { const botKey = await projectBotService.getBotKey(projectId); if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); @@ -1148,7 +1149,8 @@ export const secretServiceFactory = ({ secretCommentCiphertext: secretCommentEncrypted.ciphertext, secretCommentIV: secretCommentEncrypted.iv, secretCommentTag: secretCommentEncrypted.tag, - skipMultilineEncoding + skipMultilineEncoding, + tags: tagIds }); return decryptSecretRaw(secret, botKey); @@ -1165,7 +1167,8 @@ export const secretServiceFactory = ({ type, secretPath, secretValue, - skipMultilineEncoding + skipMultilineEncoding, + tagIds }: TUpdateSecretRawDTO) => { const botKey = await projectBotService.getBotKey(projectId); if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); @@ -1185,7 +1188,8 @@ export const secretServiceFactory = ({ secretValueCiphertext: secretValueEncrypted.ciphertext, secretValueIV: secretValueEncrypted.iv, secretValueTag: secretValueEncrypted.tag, - skipMultilineEncoding + skipMultilineEncoding, + tags: tagIds }); await snapshotService.performSnapshot(secret.folderId); diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 18a0077fe..1aac324c5 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -164,6 +164,7 @@ export type TCreateSecretRawDTO = TProjectPermission & { secretName: string; secretValue: string; type: SecretType; + tagIds?: string[]; secretComment?: string; skipMultilineEncoding?: boolean; }; @@ -174,6 +175,7 @@ export type TUpdateSecretRawDTO = TProjectPermission & { secretName: string; secretValue?: string; type: SecretType; + tagIds?: string[]; skipMultilineEncoding?: boolean; secretReminderRepeatDays?: number | null; secretReminderNote?: string | null;