feat: changed from token to headers for audit log streams api

This commit is contained in:
Akhil Mohan
2024-05-03 17:43:14 +05:30
parent f369761920
commit 2dcb409d3b
9 changed files with 242 additions and 99 deletions

View File

@@ -8,11 +8,11 @@ export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable(TableName.AuditLogStream, (t) => { await knex.schema.createTable(TableName.AuditLogStream, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("url").notNullable(); t.string("url").notNullable();
t.text("encryptedTokenCiphertext"); t.text("encryptedHeadersCiphertext");
t.text("encryptedTokenIV"); t.text("encryptedHeadersIV");
t.text("encryptedTokenTag"); t.text("encryptedHeadersTag");
t.string("encryptedTokenAlgorithm"); t.string("encryptedHeadersAlgorithm");
t.string("encryptedTokenKeyEncoding"); t.string("encryptedHeadersKeyEncoding");
t.uuid("orgId").notNullable(); t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true); t.timestamps(true, true, true);

View File

@@ -10,11 +10,11 @@ import { TImmutableDBKeys } from "./models";
export const AuditLogStreamsSchema = z.object({ export const AuditLogStreamsSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
url: z.string(), url: z.string(),
encryptedTokenCiphertext: z.string().nullable().optional(), encryptedHeadersCiphertext: z.string().nullable().optional(),
encryptedTokenIV: z.string().nullable().optional(), encryptedHeadersIV: z.string().nullable().optional(),
encryptedTokenTag: z.string().nullable().optional(), encryptedHeadersTag: z.string().nullable().optional(),
encryptedTokenAlgorithm: z.string().nullable().optional(), encryptedHeadersAlgorithm: z.string().nullable().optional(),
encryptedTokenKeyEncoding: z.string().nullable().optional(), encryptedHeadersKeyEncoding: z.string().nullable().optional(),
orgId: z.string().uuid(), orgId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date()

View File

@@ -22,7 +22,14 @@ export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) =
], ],
body: z.object({ body: z.object({
url: z.string().min(1).describe(AUDIT_LOG_STREAMS.CREATE.url), 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: { response: {
200: z.object({ 200: z.object({
@@ -38,7 +45,7 @@ export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) =
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
url: req.body.url, url: req.body.url,
token: req.body.token headers: req.body.headers
}); });
return { auditLogStream }; return { auditLogStream };
@@ -63,7 +70,14 @@ export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) =
}), }),
body: z.object({ body: z.object({
url: z.string().optional().describe(AUDIT_LOG_STREAMS.UPDATE.url), 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: { response: {
200: z.object({ 200: z.object({
@@ -80,7 +94,7 @@ export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) =
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
id: req.params.id, id: req.params.id,
url: req.body.url, url: req.body.url,
token: req.body.token headers: req.body.headers
}); });
return { auditLogStream }; return { auditLogStream };
@@ -141,7 +155,15 @@ export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) =
}), }),
response: { response: {
200: z.object({ 200: z.object({
auditLogStream: SanitizedAuditLogStreamSchema.extend({ token: z.string().optional() }) auditLogStream: SanitizedAuditLogStreamSchema.extend({
headers: z
.object({
key: z.string(),
value: z.string()
})
.array()
.optional()
})
}) })
} }
}, },

View File

@@ -13,6 +13,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-p
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal"; import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal";
import { import {
LogStreamHeaders,
TCreateAuditLogStreamDTO, TCreateAuditLogStreamDTO,
TDeleteAuditLogStreamDTO, TDeleteAuditLogStreamDTO,
TGetDetailsAuditLogStreamDTO, TGetDetailsAuditLogStreamDTO,
@@ -33,7 +34,14 @@ export const auditLogStreamServiceFactory = ({
permissionService, permissionService,
licenseService licenseService
}: TAuditLogStreamServiceFactoryDep) => { }: 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" }); if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@@ -62,30 +70,37 @@ export const auditLogStreamServiceFactory = ({
} }
// testing connection first // testing connection first
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
if (token) headers.Authorization = `Bearer ${token}`; if (headers.length)
await request.post( headers.forEach(({ key, value }) => {
url, streamHeaders[key] = value;
{ ping: "ok" }, });
{ await request
headers, .post(
// request timeout url,
timeout: AUDIT_LOG_STREAM_TIMEOUT, { ping: "ok" },
// connection timeout {
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) headers: streamHeaders,
} // request timeout
); timeout: AUDIT_LOG_STREAM_TIMEOUT,
const encryptedToken = token ? infisicalSymmetricEncypt(token) : undefined; // 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({ const logStream = await auditLogStreamDAL.create({
orgId: actorOrgId, orgId: actorOrgId,
url, url,
...(encryptedToken ...(encryptedHeaders
? { ? {
encryptedTokenCiphertext: encryptedToken.ciphertext, encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
encryptedTokenIV: encryptedToken.iv, encryptedHeadersIV: encryptedHeaders.iv,
encryptedTokenTag: encryptedToken.tag, encryptedHeadersTag: encryptedHeaders.tag,
encryptedTokenAlgorithm: encryptedToken.algorithm, encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
encryptedTokenKeyEncoding: encryptedToken.encoding encryptedHeadersKeyEncoding: encryptedHeaders.encoding
} }
: {}) : {})
}); });
@@ -96,7 +111,7 @@ export const auditLogStreamServiceFactory = ({
id, id,
url, url,
actor, actor,
token, headers = [],
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod actorAuthMethod
@@ -119,13 +134,17 @@ export const auditLogStreamServiceFactory = ({
if (url) validateLocalIps(url); if (url) validateLocalIps(url);
// testing connection first // testing connection first
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
if (token) headers.Authorization = `Bearer ${token}`; if (headers.length)
headers.forEach(({ key, value }) => {
streamHeaders[key] = value;
});
await request.post( await request.post(
url || logStream.url, url || logStream.url,
{ ping: "ok" }, { ping: "ok" },
{ {
headers, headers: streamHeaders,
// request timeout // request timeout
timeout: AUDIT_LOG_STREAM_TIMEOUT, timeout: AUDIT_LOG_STREAM_TIMEOUT,
// connection 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, { const updatedLogStream = await auditLogStreamDAL.updateById(id, {
url, url,
...(encryptedToken ...(encryptedHeaders
? { ? {
encryptedTokenCiphertext: encryptedToken.ciphertext, encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
encryptedTokenIV: encryptedToken.iv, encryptedHeadersIV: encryptedHeaders.iv,
encryptedTokenTag: encryptedToken.tag, encryptedHeadersTag: encryptedHeaders.tag,
encryptedTokenAlgorithm: encryptedToken.algorithm, encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
encryptedTokenKeyEncoding: encryptedToken.encoding encryptedHeadersKeyEncoding: encryptedHeaders.encoding
} }
: {}) : {})
}); });
@@ -171,17 +190,19 @@ export const auditLogStreamServiceFactory = ({
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
const token = const headers =
logStream?.encryptedTokenCiphertext && logStream?.encryptedTokenIV && logStream?.encryptedTokenTag logStream?.encryptedHeadersCiphertext && logStream?.encryptedHeadersIV && logStream?.encryptedHeadersTag
? infisicalSymmetricDecrypt({ ? (JSON.parse(
tag: logStream.encryptedTokenTag, infisicalSymmetricDecrypt({
iv: logStream.encryptedTokenIV, tag: logStream.encryptedHeadersTag,
ciphertext: logStream.encryptedTokenCiphertext, iv: logStream.encryptedHeadersIV,
keyEncoding: logStream.encryptedTokenKeyEncoding as SecretKeyEncoding ciphertext: logStream.encryptedHeadersCiphertext,
}) keyEncoding: logStream.encryptedHeadersKeyEncoding as SecretKeyEncoding
})
) as LogStreamHeaders[])
: undefined; : undefined;
return { ...logStream, token }; return { ...logStream, headers };
}; };
const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListAuditLogStreamDTO) => { const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListAuditLogStreamDTO) => {

View File

@@ -1,14 +1,19 @@
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
export type LogStreamHeaders = {
key: string;
value: string;
};
export type TCreateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & { export type TCreateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
url: string; url: string;
token?: string; headers?: LogStreamHeaders[];
}; };
export type TUpdateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & { export type TUpdateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
id: string; id: string;
url?: string; url?: string;
token?: string; headers?: LogStreamHeaders[];
}; };
export type TDeleteAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & { export type TDeleteAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {

View File

@@ -8,6 +8,7 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TAuditLogStreamDALFactory } from "../audit-log-stream/audit-log-stream-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 { TLicenseServiceFactory } from "../license/license-service";
import { TAuditLogDALFactory } from "./audit-log-dal"; import { TAuditLogDALFactory } from "./audit-log-dal";
import { TCreateAuditLogDTO } from "./audit-log-types"; import { TCreateAuditLogDTO } from "./audit-log-types";
@@ -74,20 +75,31 @@ export const auditLogQueueServiceFactory = ({
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : []; const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
await Promise.allSettled( await Promise.allSettled(
logStreams.map( logStreams.map(
async ({ url, encryptedTokenTag, encryptedTokenIV, encryptedTokenKeyEncoding, encryptedTokenCiphertext }) => { async ({
const token = url,
encryptedTokenIV && encryptedTokenCiphertext && encryptedTokenTag encryptedHeadersTag,
? infisicalSymmetricDecrypt({ encryptedHeadersIV,
keyEncoding: encryptedTokenKeyEncoding as SecretKeyEncoding, encryptedHeadersKeyEncoding,
iv: encryptedTokenIV, encryptedHeadersCiphertext
tag: encryptedTokenTag, }) => {
ciphertext: encryptedTokenCiphertext const streamHeaders =
}) encryptedHeadersIV && encryptedHeadersCiphertext && encryptedHeadersTag
: undefined; ? (JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: encryptedHeadersKeyEncoding as SecretKeyEncoding,
iv: encryptedHeadersIV,
tag: encryptedHeadersTag,
ciphertext: encryptedHeadersCiphertext
})
) as LogStreamHeaders[])
: [];
const headers: RawAxiosRequestHeaders = { "Content-Type": "application/json" }; 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, { return request.post(url, auditLog, {
headers, headers,

View File

@@ -618,12 +618,20 @@ export const INTEGRATION = {
export const AUDIT_LOG_STREAMS = { export const AUDIT_LOG_STREAMS = {
CREATE: { CREATE: {
url: "The HTTP URL to push logs to.", 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: { UPDATE: {
id: "The ID of the audit log stream to update.", id: "The ID of the audit log stream to update.",
url: "The HTTP URL to push logs to.", 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: { DELETE: {
id: "The ID of the audit log stream to delete." id: "The ID of the audit log stream to delete."

View File

@@ -1,19 +1,24 @@
export type LogStreamHeaders = {
key: string;
value: string;
};
export type TAuditLogStream = { export type TAuditLogStream = {
id: string; id: string;
url: string; url: string;
token: string; headers?: LogStreamHeaders[];
}; };
export type TCreateAuditLogStreamDTO = { export type TCreateAuditLogStreamDTO = {
url: string; url: string;
token?: string; headers?: LogStreamHeaders[];
orgId: string; orgId: string;
}; };
export type TUpdateAuditLogStreamDTO = { export type TUpdateAuditLogStreamDTO = {
id: string; id: string;
url?: string; url?: string;
token?: string; headers?: LogStreamHeaders[];
orgId: string; orgId: string;
}; };

View File

@@ -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 { z } from "zod";
import { createNotification } from "@app/components/notifications"; 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 { useOrganization } from "@app/context";
import { import {
useCreateAuditLogStream, useCreateAuditLogStream,
@@ -17,7 +19,13 @@ type Props = {
const formSchema = z.object({ const formSchema = z.object({
url: z.string().url().min(1), 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<typeof formSchema>; type TForm = z.infer<typeof formSchema>;
@@ -33,18 +41,28 @@ export const AuditLogStreamForm = ({ id = "", onClose }: Props) => {
const { const {
handleSubmit, handleSubmit,
control, control,
setValue,
getValues,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<TForm>({ } = useForm<TForm>({
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; if (!id) return;
try { try {
await updateAuditLogStream.mutateAsync({ await updateAuditLogStream.mutateAsync({
id, id,
orgId, orgId,
token, headers,
url url
}); });
createNotification({ 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; if (isSubmitting) return;
const sanitizedHeaders = headers.filter(({ key, value }) => Boolean(key) && Boolean(value));
const streamHeaders = sanitizedHeaders.length ? sanitizedHeaders : undefined;
if (isEdit) { if (isEdit) {
await handleAuditLogStreamEdit({ token, url }); await handleAuditLogStreamEdit({ headers: streamHeaders, url });
return; return;
} }
try { try {
await createAuditLogStream.mutateAsync({ await createAuditLogStream.mutateAsync({
orgId, orgId,
token, headers: streamHeaders,
url url
}); });
createNotification({ createNotification({
@@ -96,32 +116,82 @@ export const AuditLogStreamForm = ({ id = "", onClose }: Props) => {
} }
return ( return (
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit(handleFormSubmit)} autoComplete="off">
<div> <div>
<Controller <Controller
control={control} control={control}
name="url" name="url"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Endpoint URL" isError={Boolean(error?.message)} errorText={error?.message}> <FormControl
label="Endpoint URL"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
)} )}
/> />
<Controller <FormLabel label="Headers" isOptional />
control={control} {headerFields.fields.map(({ id: headerFieldId }, i) => (
name="token" <div key={headerFieldId} className="flex space-x-2">
render={({ field, fieldState: { error } }) => ( <Controller
<FormControl control={control}
label="Token" name={`headers.${i}.key`}
isOptional render={({ field, fieldState: { error } }) => (
isError={Boolean(error?.message)} <FormControl
errorText={error?.message} isError={Boolean(error?.message)}
helperText="The bearer token used to authenticate with the logging provider endpoint" errorText={error?.message}
className="w-1/3"
>
<Input {...field} placeholder="Authorization" />
</FormControl>
)}
/>
<Controller
control={control}
name={`headers.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="flex-grow"
>
<Input
{...field}
type="password"
placeholder="Bearer <token>"
autoComplete="new-password"
/>
</FormControl>
)}
/>
<IconButton
ariaLabel="delete key"
className="h-9"
variant="outline_bg"
onClick={() => {
const header = getValues("headers");
if (header && header?.length > 1) {
headerFields.remove(i);
} else {
setValue("headers", [{ key: "", value: "" }]);
}
}}
> >
<Input {...field} type="password" /> <FontAwesomeIcon icon={faTrash} />
</FormControl> </IconButton>
)} </div>
/> ))}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => headerFields.append({ value: "", key: "" })}
>
Add Key
</Button>
</div>
</div> </div>
<div className="mt-8 flex items-center"> <div className="mt-8 flex items-center">
<Button className="mr-4" type="submit" isLoading={isSubmitting}> <Button className="mr-4" type="submit" isLoading={isSubmitting}>