From 2dcb409d3b95c648e4f7365b13679b5207c651f5 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Fri, 3 May 2024 17:43:14 +0530 Subject: [PATCH] feat: changed from token to headers for audit log streams api --- ....ts => 20240503101144_audit-log-stream.ts} | 10 +- backend/src/db/schemas/audit-log-streams.ts | 10 +- .../ee/routes/v1/audit-log-stream-router.ts | 32 ++++- .../audit-log-stream-service.ts | 103 +++++++++------ .../audit-log-stream-types.ts | 9 +- .../ee/services/audit-log/audit-log-queue.ts | 34 +++-- backend/src/lib/api-docs/constants.ts | 12 +- .../src/hooks/api/auditLogStreams/types.ts | 11 +- .../AuditLogStreamTab/AuditLogStreamForm.tsx | 120 ++++++++++++++---- 9 files changed, 242 insertions(+), 99 deletions(-) rename backend/src/db/migrations/{20240423070528_audit-log-stream.ts => 20240503101144_audit-log-stream.ts} (80%) diff --git a/backend/src/db/migrations/20240423070528_audit-log-stream.ts b/backend/src/db/migrations/20240503101144_audit-log-stream.ts similarity index 80% rename from backend/src/db/migrations/20240423070528_audit-log-stream.ts rename to backend/src/db/migrations/20240503101144_audit-log-stream.ts index c4133fb94..210ee1bfa 100644 --- a/backend/src/db/migrations/20240423070528_audit-log-stream.ts +++ b/backend/src/db/migrations/20240503101144_audit-log-stream.ts @@ -8,11 +8,11 @@ export async function up(knex: Knex): Promise { await knex.schema.createTable(TableName.AuditLogStream, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.string("url").notNullable(); - t.text("encryptedTokenCiphertext"); - t.text("encryptedTokenIV"); - t.text("encryptedTokenTag"); - t.string("encryptedTokenAlgorithm"); - t.string("encryptedTokenKeyEncoding"); + t.text("encryptedHeadersCiphertext"); + t.text("encryptedHeadersIV"); + t.text("encryptedHeadersTag"); + t.string("encryptedHeadersAlgorithm"); + t.string("encryptedHeadersKeyEncoding"); t.uuid("orgId").notNullable(); t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); t.timestamps(true, true, true); diff --git a/backend/src/db/schemas/audit-log-streams.ts b/backend/src/db/schemas/audit-log-streams.ts index bab73d0c5..901dd8d27 100644 --- a/backend/src/db/schemas/audit-log-streams.ts +++ b/backend/src/db/schemas/audit-log-streams.ts @@ -10,11 +10,11 @@ import { TImmutableDBKeys } from "./models"; export const AuditLogStreamsSchema = z.object({ id: z.string().uuid(), url: z.string(), - encryptedTokenCiphertext: z.string().nullable().optional(), - encryptedTokenIV: z.string().nullable().optional(), - encryptedTokenTag: z.string().nullable().optional(), - encryptedTokenAlgorithm: z.string().nullable().optional(), - encryptedTokenKeyEncoding: z.string().nullable().optional(), + encryptedHeadersCiphertext: z.string().nullable().optional(), + encryptedHeadersIV: z.string().nullable().optional(), + encryptedHeadersTag: z.string().nullable().optional(), + encryptedHeadersAlgorithm: z.string().nullable().optional(), + encryptedHeadersKeyEncoding: z.string().nullable().optional(), orgId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date() diff --git a/backend/src/ee/routes/v1/audit-log-stream-router.ts b/backend/src/ee/routes/v1/audit-log-stream-router.ts index d11b74cc3..17bd9e64b 100644 --- a/backend/src/ee/routes/v1/audit-log-stream-router.ts +++ b/backend/src/ee/routes/v1/audit-log-stream-router.ts @@ -22,7 +22,14 @@ export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) = ], body: z.object({ url: z.string().min(1).describe(AUDIT_LOG_STREAMS.CREATE.url), - token: z.string().optional().describe(AUDIT_LOG_STREAMS.CREATE.token) + headers: z + .object({ + key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.key), + value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.CREATE.headers.value) + }) + .describe(AUDIT_LOG_STREAMS.CREATE.headers.desc) + .array() + .optional() }), response: { 200: z.object({ @@ -38,7 +45,7 @@ export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) = actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, url: req.body.url, - token: req.body.token + headers: req.body.headers }); return { auditLogStream }; @@ -63,7 +70,14 @@ export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) = }), body: z.object({ url: z.string().optional().describe(AUDIT_LOG_STREAMS.UPDATE.url), - token: z.string().optional().describe(AUDIT_LOG_STREAMS.UPDATE.token) + headers: z + .object({ + key: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.key), + value: z.string().min(1).trim().describe(AUDIT_LOG_STREAMS.UPDATE.headers.value) + }) + .describe(AUDIT_LOG_STREAMS.UPDATE.headers.desc) + .array() + .optional() }), response: { 200: z.object({ @@ -80,7 +94,7 @@ export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) = actorAuthMethod: req.permission.authMethod, id: req.params.id, url: req.body.url, - token: req.body.token + headers: req.body.headers }); return { auditLogStream }; @@ -141,7 +155,15 @@ export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) = }), response: { 200: z.object({ - auditLogStream: SanitizedAuditLogStreamSchema.extend({ token: z.string().optional() }) + auditLogStream: SanitizedAuditLogStreamSchema.extend({ + headers: z + .object({ + key: z.string(), + value: z.string() + }) + .array() + .optional() + }) }) } }, diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts index fd6e10a28..b82ce04ae 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-service.ts @@ -13,6 +13,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-p import { TPermissionServiceFactory } from "../permission/permission-service"; import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal"; import { + LogStreamHeaders, TCreateAuditLogStreamDTO, TDeleteAuditLogStreamDTO, TGetDetailsAuditLogStreamDTO, @@ -33,7 +34,14 @@ export const auditLogStreamServiceFactory = ({ permissionService, licenseService }: TAuditLogStreamServiceFactoryDep) => { - const create = async ({ url, actor, token, actorId, actorOrgId, actorAuthMethod }: TCreateAuditLogStreamDTO) => { + const create = async ({ + url, + actor, + headers = [], + actorId, + actorOrgId, + actorAuthMethod + }: TCreateAuditLogStreamDTO) => { if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" }); const plan = await licenseService.getPlan(actorOrgId); @@ -62,30 +70,37 @@ export const auditLogStreamServiceFactory = ({ } // testing connection first - const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; - if (token) headers.Authorization = `Bearer ${token}`; - await request.post( - url, - { ping: "ok" }, - { - headers, - // request timeout - timeout: AUDIT_LOG_STREAM_TIMEOUT, - // connection timeout - signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) - } - ); - const encryptedToken = token ? infisicalSymmetricEncypt(token) : undefined; + const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; + if (headers.length) + headers.forEach(({ key, value }) => { + streamHeaders[key] = value; + }); + await request + .post( + url, + { ping: "ok" }, + { + headers: streamHeaders, + // request timeout + timeout: AUDIT_LOG_STREAM_TIMEOUT, + // connection timeout + signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) + } + ) + .catch((err) => { + throw new Error(`Failed to connect with the source ${(err as Error)?.message}`); + }); + const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined; const logStream = await auditLogStreamDAL.create({ orgId: actorOrgId, url, - ...(encryptedToken + ...(encryptedHeaders ? { - encryptedTokenCiphertext: encryptedToken.ciphertext, - encryptedTokenIV: encryptedToken.iv, - encryptedTokenTag: encryptedToken.tag, - encryptedTokenAlgorithm: encryptedToken.algorithm, - encryptedTokenKeyEncoding: encryptedToken.encoding + encryptedHeadersCiphertext: encryptedHeaders.ciphertext, + encryptedHeadersIV: encryptedHeaders.iv, + encryptedHeadersTag: encryptedHeaders.tag, + encryptedHeadersAlgorithm: encryptedHeaders.algorithm, + encryptedHeadersKeyEncoding: encryptedHeaders.encoding } : {}) }); @@ -96,7 +111,7 @@ export const auditLogStreamServiceFactory = ({ id, url, actor, - token, + headers = [], actorId, actorOrgId, actorAuthMethod @@ -119,13 +134,17 @@ export const auditLogStreamServiceFactory = ({ if (url) validateLocalIps(url); // testing connection first - const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; - if (token) headers.Authorization = `Bearer ${token}`; + const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; + if (headers.length) + headers.forEach(({ key, value }) => { + streamHeaders[key] = value; + }); + await request.post( url || logStream.url, { ping: "ok" }, { - headers, + headers: streamHeaders, // request timeout timeout: AUDIT_LOG_STREAM_TIMEOUT, // connection timeout @@ -133,16 +152,16 @@ export const auditLogStreamServiceFactory = ({ } ); - const encryptedToken = token ? infisicalSymmetricEncypt(token) : undefined; + const encryptedHeaders = headers ? infisicalSymmetricEncypt(JSON.stringify(headers)) : undefined; const updatedLogStream = await auditLogStreamDAL.updateById(id, { url, - ...(encryptedToken + ...(encryptedHeaders ? { - encryptedTokenCiphertext: encryptedToken.ciphertext, - encryptedTokenIV: encryptedToken.iv, - encryptedTokenTag: encryptedToken.tag, - encryptedTokenAlgorithm: encryptedToken.algorithm, - encryptedTokenKeyEncoding: encryptedToken.encoding + encryptedHeadersCiphertext: encryptedHeaders.ciphertext, + encryptedHeadersIV: encryptedHeaders.iv, + encryptedHeadersTag: encryptedHeaders.tag, + encryptedHeadersAlgorithm: encryptedHeaders.algorithm, + encryptedHeadersKeyEncoding: encryptedHeaders.encoding } : {}) }); @@ -171,17 +190,19 @@ export const auditLogStreamServiceFactory = ({ const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); - const token = - logStream?.encryptedTokenCiphertext && logStream?.encryptedTokenIV && logStream?.encryptedTokenTag - ? infisicalSymmetricDecrypt({ - tag: logStream.encryptedTokenTag, - iv: logStream.encryptedTokenIV, - ciphertext: logStream.encryptedTokenCiphertext, - keyEncoding: logStream.encryptedTokenKeyEncoding as SecretKeyEncoding - }) + const headers = + logStream?.encryptedHeadersCiphertext && logStream?.encryptedHeadersIV && logStream?.encryptedHeadersTag + ? (JSON.parse( + infisicalSymmetricDecrypt({ + tag: logStream.encryptedHeadersTag, + iv: logStream.encryptedHeadersIV, + ciphertext: logStream.encryptedHeadersCiphertext, + keyEncoding: logStream.encryptedHeadersKeyEncoding as SecretKeyEncoding + }) + ) as LogStreamHeaders[]) : undefined; - return { ...logStream, token }; + return { ...logStream, headers }; }; const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListAuditLogStreamDTO) => { diff --git a/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts b/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts index ec8c14923..3c22251d7 100644 --- a/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts +++ b/backend/src/ee/services/audit-log-stream/audit-log-stream-types.ts @@ -1,14 +1,19 @@ import { TOrgPermission } from "@app/lib/types"; +export type LogStreamHeaders = { + key: string; + value: string; +}; + export type TCreateAuditLogStreamDTO = Omit & { url: string; - token?: string; + headers?: LogStreamHeaders[]; }; export type TUpdateAuditLogStreamDTO = Omit & { id: string; url?: string; - token?: string; + headers?: LogStreamHeaders[]; }; export type TDeleteAuditLogStreamDTO = Omit & { diff --git a/backend/src/ee/services/audit-log/audit-log-queue.ts b/backend/src/ee/services/audit-log/audit-log-queue.ts index a4d504d35..fa9a0a500 100644 --- a/backend/src/ee/services/audit-log/audit-log-queue.ts +++ b/backend/src/ee/services/audit-log/audit-log-queue.ts @@ -8,6 +8,7 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TAuditLogStreamDALFactory } from "../audit-log-stream/audit-log-stream-dal"; +import { LogStreamHeaders } from "../audit-log-stream/audit-log-stream-types"; import { TLicenseServiceFactory } from "../license/license-service"; import { TAuditLogDALFactory } from "./audit-log-dal"; import { TCreateAuditLogDTO } from "./audit-log-types"; @@ -74,20 +75,31 @@ export const auditLogQueueServiceFactory = ({ const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : []; await Promise.allSettled( logStreams.map( - async ({ url, encryptedTokenTag, encryptedTokenIV, encryptedTokenKeyEncoding, encryptedTokenCiphertext }) => { - const token = - encryptedTokenIV && encryptedTokenCiphertext && encryptedTokenTag - ? infisicalSymmetricDecrypt({ - keyEncoding: encryptedTokenKeyEncoding as SecretKeyEncoding, - iv: encryptedTokenIV, - tag: encryptedTokenTag, - ciphertext: encryptedTokenCiphertext - }) - : undefined; + async ({ + url, + encryptedHeadersTag, + encryptedHeadersIV, + encryptedHeadersKeyEncoding, + encryptedHeadersCiphertext + }) => { + const streamHeaders = + encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag + ? (JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding, + iv: encryptedHeadersIV, + tag: encryptedHeadersTag, + ciphertext: encryptedHeadersCiphertext + }) + ) as LogStreamHeaders[]) + : []; const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; - if (token) headers.Authorization = `Bearer ${token}`; + if (headers.length) + streamHeaders.forEach(({ key, value }) => { + headers[key] = value; + }); return request.post(url, auditLog, { headers, diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b4c0c2a5c..39728ba7b 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -618,12 +618,20 @@ export const INTEGRATION = { export const AUDIT_LOG_STREAMS = { CREATE: { url: "The HTTP URL to push logs to.", - token: "Authentication token for the external provider used for identification." + headers: { + desc: "The HTTP headers attached for the external prrovider requests.", + key: "The HTTP header key name.", + value: "The HTTP header value." + } }, UPDATE: { id: "The ID of the audit log stream to update.", url: "The HTTP URL to push logs to.", - token: "Authentication token for the external provider used for identification." + headers: { + desc: "The HTTP headers attached for the external prrovider requests.", + key: "The HTTP header key name.", + value: "The HTTP header value." + } }, DELETE: { id: "The ID of the audit log stream to delete." diff --git a/frontend/src/hooks/api/auditLogStreams/types.ts b/frontend/src/hooks/api/auditLogStreams/types.ts index 7e73ae301..8e21a3209 100644 --- a/frontend/src/hooks/api/auditLogStreams/types.ts +++ b/frontend/src/hooks/api/auditLogStreams/types.ts @@ -1,19 +1,24 @@ +export type LogStreamHeaders = { + key: string; + value: string; +}; + export type TAuditLogStream = { id: string; url: string; - token: string; + headers?: LogStreamHeaders[]; }; export type TCreateAuditLogStreamDTO = { url: string; - token?: string; + headers?: LogStreamHeaders[]; orgId: string; }; export type TUpdateAuditLogStreamDTO = { id: string; url?: string; - token?: string; + headers?: LogStreamHeaders[]; orgId: string; }; diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/AuditLogStreamTab/AuditLogStreamForm.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/AuditLogStreamTab/AuditLogStreamForm.tsx index 66acea296..18bfa6340 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/components/AuditLogStreamTab/AuditLogStreamForm.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/components/AuditLogStreamTab/AuditLogStreamForm.tsx @@ -1,8 +1,10 @@ -import { Controller, useForm } from "react-hook-form"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Input, Spinner } from "@app/components/v2"; +import { Button, FormControl, FormLabel, IconButton, Input, Spinner } from "@app/components/v2"; import { useOrganization } from "@app/context"; import { useCreateAuditLogStream, @@ -17,7 +19,13 @@ type Props = { const formSchema = z.object({ url: z.string().url().min(1), - token: z.string().optional() + headers: z + .object({ + key: z.string(), + value: z.string() + }) + .array() + .optional() }); type TForm = z.infer; @@ -33,18 +41,28 @@ export const AuditLogStreamForm = ({ id = "", onClose }: Props) => { const { handleSubmit, control, + setValue, + getValues, formState: { isSubmitting } } = useForm({ - values: auditLogStream?.data + values: auditLogStream?.data, + defaultValues: { + headers: [{ key: "", value: "" }] + } }); - const handleAuditLogStreamEdit = async ({ token, url }: TForm) => { + const headerFields = useFieldArray({ + control, + name: "headers" + }); + + const handleAuditLogStreamEdit = async ({ headers, url }: TForm) => { if (!id) return; try { await updateAuditLogStream.mutateAsync({ id, orgId, - token, + headers, url }); createNotification({ @@ -61,16 +79,18 @@ export const AuditLogStreamForm = ({ id = "", onClose }: Props) => { } }; - const handleFormSubmit = async ({ token, url }: TForm) => { + const handleFormSubmit = async ({ headers = [], url }: TForm) => { if (isSubmitting) return; + const sanitizedHeaders = headers.filter(({ key, value }) => Boolean(key) && Boolean(value)); + const streamHeaders = sanitizedHeaders.length ? sanitizedHeaders : undefined; if (isEdit) { - await handleAuditLogStreamEdit({ token, url }); + await handleAuditLogStreamEdit({ headers: streamHeaders, url }); return; } try { await createAuditLogStream.mutateAsync({ orgId, - token, + headers: streamHeaders, url }); createNotification({ @@ -96,32 +116,82 @@ export const AuditLogStreamForm = ({ id = "", onClose }: Props) => { } return ( -
+
( - + )} /> - ( - + {headerFields.fields.map(({ id: headerFieldId }, i) => ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + { + const header = getValues("headers"); + if (header && header?.length > 1) { + headerFields.remove(i); + } else { + setValue("headers", [{ key: "", value: "" }]); + } + }} > - - - )} - /> + + +
+ ))} +
+ +