Merge remote-tracking branch 'origin'
@@ -3,6 +3,7 @@ import "fastify";
|
|||||||
import { TUsers } from "@app/db/schemas";
|
import { TUsers } from "@app/db/schemas";
|
||||||
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||||
@@ -120,6 +121,7 @@ declare module "fastify" {
|
|||||||
scim: TScimServiceFactory;
|
scim: TScimServiceFactory;
|
||||||
ldap: TLdapConfigServiceFactory;
|
ldap: TLdapConfigServiceFactory;
|
||||||
auditLog: TAuditLogServiceFactory;
|
auditLog: TAuditLogServiceFactory;
|
||||||
|
auditLogStream: TAuditLogStreamServiceFactory;
|
||||||
secretScanning: TSecretScanningServiceFactory;
|
secretScanning: TSecretScanningServiceFactory;
|
||||||
license: TLicenseServiceFactory;
|
license: TLicenseServiceFactory;
|
||||||
trustedIp: TTrustedIpServiceFactory;
|
trustedIp: TTrustedIpServiceFactory;
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import {
|
|||||||
TApiKeysUpdate,
|
TApiKeysUpdate,
|
||||||
TAuditLogs,
|
TAuditLogs,
|
||||||
TAuditLogsInsert,
|
TAuditLogsInsert,
|
||||||
|
TAuditLogStreams,
|
||||||
|
TAuditLogStreamsInsert,
|
||||||
|
TAuditLogStreamsUpdate,
|
||||||
TAuditLogsUpdate,
|
TAuditLogsUpdate,
|
||||||
TAuthTokens,
|
TAuthTokens,
|
||||||
TAuthTokenSessions,
|
TAuthTokenSessions,
|
||||||
@@ -404,6 +407,11 @@ declare module "knex/types/tables" {
|
|||||||
[TableName.LdapGroupMap]: Knex.CompositeTableType<TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate>;
|
[TableName.LdapGroupMap]: Knex.CompositeTableType<TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate>;
|
||||||
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
|
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;
|
||||||
[TableName.AuditLog]: Knex.CompositeTableType<TAuditLogs, TAuditLogsInsert, TAuditLogsUpdate>;
|
[TableName.AuditLog]: Knex.CompositeTableType<TAuditLogs, TAuditLogsInsert, TAuditLogsUpdate>;
|
||||||
|
[TableName.AuditLogStream]: Knex.CompositeTableType<
|
||||||
|
TAuditLogStreams,
|
||||||
|
TAuditLogStreamsInsert,
|
||||||
|
TAuditLogStreamsUpdate
|
||||||
|
>;
|
||||||
[TableName.GitAppInstallSession]: Knex.CompositeTableType<
|
[TableName.GitAppInstallSession]: Knex.CompositeTableType<
|
||||||
TGitAppInstallSessions,
|
TGitAppInstallSessions,
|
||||||
TGitAppInstallSessionsInsert,
|
TGitAppInstallSessionsInsert,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.AuditLogStream))) {
|
||||||
|
await knex.schema.createTable(TableName.AuditLogStream, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("url").notNullable();
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.AuditLogStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.AuditLogStream);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.AuditLogStream);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const AuditLogStreamsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
url: z.string(),
|
||||||
|
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()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TAuditLogStreams = z.infer<typeof AuditLogStreamsSchema>;
|
||||||
|
export type TAuditLogStreamsInsert = Omit<z.input<typeof AuditLogStreamsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TAuditLogStreamsUpdate = Partial<Omit<z.input<typeof AuditLogStreamsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./api-keys";
|
export * from "./api-keys";
|
||||||
|
export * from "./audit-log-streams";
|
||||||
export * from "./audit-logs";
|
export * from "./audit-logs";
|
||||||
export * from "./auth-token-sessions";
|
export * from "./auth-token-sessions";
|
||||||
export * from "./auth-tokens";
|
export * from "./auth-tokens";
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export enum TableName {
|
|||||||
LdapConfig = "ldap_configs",
|
LdapConfig = "ldap_configs",
|
||||||
LdapGroupMap = "ldap_group_maps",
|
LdapGroupMap = "ldap_group_maps",
|
||||||
AuditLog = "audit_logs",
|
AuditLog = "audit_logs",
|
||||||
|
AuditLogStream = "audit_log_streams",
|
||||||
GitAppInstallSession = "git_app_install_sessions",
|
GitAppInstallSession = "git_app_install_sessions",
|
||||||
GitAppOrg = "git_app_org",
|
GitAppOrg = "git_app_org",
|
||||||
SecretScanningGitRisk = "secret_scanning_git_risks",
|
SecretScanningGitRisk = "secret_scanning_git_risks",
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AUDIT_LOG_STREAMS } from "@app/lib/api-docs";
|
||||||
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { SanitizedAuditLogStreamSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Create an Audit Log Stream.",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
body: z.object({
|
||||||
|
url: z.string().min(1).describe(AUDIT_LOG_STREAMS.CREATE.url),
|
||||||
|
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({
|
||||||
|
auditLogStream: SanitizedAuditLogStreamSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const auditLogStream = await server.services.auditLogStream.create({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
url: req.body.url,
|
||||||
|
headers: req.body.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
return { auditLogStream };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Update an Audit Log Stream by ID.",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
id: z.string().describe(AUDIT_LOG_STREAMS.UPDATE.id)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
url: z.string().optional().describe(AUDIT_LOG_STREAMS.UPDATE.url),
|
||||||
|
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({
|
||||||
|
auditLogStream: SanitizedAuditLogStreamSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const auditLogStream = await server.services.auditLogStream.updateById({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
id: req.params.id,
|
||||||
|
url: req.body.url,
|
||||||
|
headers: req.body.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
return { auditLogStream };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Delete an Audit Log Stream by ID.",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
id: z.string().describe(AUDIT_LOG_STREAMS.DELETE.id)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
auditLogStream: SanitizedAuditLogStreamSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const auditLogStream = await server.services.auditLogStream.deleteById({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return { auditLogStream };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get an Audit Log Stream by ID.",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
id: z.string().describe(AUDIT_LOG_STREAMS.GET_BY_ID.id)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
auditLogStream: SanitizedAuditLogStreamSchema.extend({
|
||||||
|
headers: z
|
||||||
|
.object({
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const auditLogStream = await server.services.auditLogStream.getById({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
|
||||||
|
return { auditLogStream };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List Audit Log Streams.",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
auditLogStreams: SanitizedAuditLogStreamSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const auditLogStreams = await server.services.auditLogStream.list({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
return { auditLogStreams };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||||
import { registerGroupRouter } from "./group-router";
|
import { registerGroupRouter } from "./group-router";
|
||||||
@@ -55,6 +56,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
|
||||||
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
|
||||||
await server.register(registerGroupRouter, { prefix: "/groups" });
|
await server.register(registerGroupRouter, { prefix: "/groups" });
|
||||||
|
await server.register(registerAuditLogStreamRouter, { prefix: "/audit-log-streams" });
|
||||||
await server.register(
|
await server.register(
|
||||||
async (privilegeRouter) => {
|
async (privilegeRouter) => {
|
||||||
await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });
|
await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TAuditLogStreamDALFactory = ReturnType<typeof auditLogStreamDALFactory>;
|
||||||
|
|
||||||
|
export const auditLogStreamDALFactory = (db: TDbClient) => {
|
||||||
|
const orm = ormify(db, TableName.AuditLogStream);
|
||||||
|
|
||||||
|
return orm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { RawAxiosRequestHeaders } from "axios";
|
||||||
|
|
||||||
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { validateLocalIps } from "@app/lib/validator";
|
||||||
|
|
||||||
|
import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal";
|
||||||
|
import {
|
||||||
|
LogStreamHeaders,
|
||||||
|
TCreateAuditLogStreamDTO,
|
||||||
|
TDeleteAuditLogStreamDTO,
|
||||||
|
TGetDetailsAuditLogStreamDTO,
|
||||||
|
TListAuditLogStreamDTO,
|
||||||
|
TUpdateAuditLogStreamDTO
|
||||||
|
} from "./audit-log-stream-types";
|
||||||
|
|
||||||
|
type TAuditLogStreamServiceFactoryDep = {
|
||||||
|
auditLogStreamDAL: TAuditLogStreamDALFactory;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAuditLogStreamServiceFactory = ReturnType<typeof auditLogStreamServiceFactory>;
|
||||||
|
|
||||||
|
export const auditLogStreamServiceFactory = ({
|
||||||
|
auditLogStreamDAL,
|
||||||
|
permissionService,
|
||||||
|
licenseService
|
||||||
|
}: TAuditLogStreamServiceFactoryDep) => {
|
||||||
|
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);
|
||||||
|
if (!plan.auditLogStreams)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create audit log streams due to plan restriction. Upgrade plan to create group."
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
validateLocalIps(url);
|
||||||
|
|
||||||
|
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
||||||
|
if (totalStreams.length >= plan.auditLogStreamLimit) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to create audit log streams due to plan limit reached. Kindly contact Infisical to add more streams."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// testing connection first
|
||||||
|
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,
|
||||||
|
...(encryptedHeaders
|
||||||
|
? {
|
||||||
|
encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
|
||||||
|
encryptedHeadersIV: encryptedHeaders.iv,
|
||||||
|
encryptedHeadersTag: encryptedHeaders.tag,
|
||||||
|
encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
|
||||||
|
encryptedHeadersKeyEncoding: encryptedHeaders.encoding
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
});
|
||||||
|
return logStream;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateById = async ({
|
||||||
|
id,
|
||||||
|
url,
|
||||||
|
actor,
|
||||||
|
headers = [],
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
}: TUpdateAuditLogStreamDTO) => {
|
||||||
|
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(actorOrgId);
|
||||||
|
if (!plan.auditLogStreams)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to update audit log streams due to plan restriction. Upgrade plan to create group."
|
||||||
|
});
|
||||||
|
|
||||||
|
const logStream = await auditLogStreamDAL.findById(id);
|
||||||
|
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
||||||
|
|
||||||
|
const { orgId } = logStream;
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
if (url) validateLocalIps(url);
|
||||||
|
|
||||||
|
// testing connection first
|
||||||
|
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: 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 updatedLogStream = await auditLogStreamDAL.updateById(id, {
|
||||||
|
url,
|
||||||
|
...(encryptedHeaders
|
||||||
|
? {
|
||||||
|
encryptedHeadersCiphertext: encryptedHeaders.ciphertext,
|
||||||
|
encryptedHeadersIV: encryptedHeaders.iv,
|
||||||
|
encryptedHeadersTag: encryptedHeaders.tag,
|
||||||
|
encryptedHeadersAlgorithm: encryptedHeaders.algorithm,
|
||||||
|
encryptedHeadersKeyEncoding: encryptedHeaders.encoding
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
});
|
||||||
|
return updatedLogStream;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TDeleteAuditLogStreamDTO) => {
|
||||||
|
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
|
||||||
|
|
||||||
|
const logStream = await auditLogStreamDAL.findById(id);
|
||||||
|
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
||||||
|
|
||||||
|
const { orgId } = logStream;
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const deletedLogStream = await auditLogStreamDAL.deleteById(id);
|
||||||
|
return deletedLogStream;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetDetailsAuditLogStreamDTO) => {
|
||||||
|
const logStream = await auditLogStreamDAL.findById(id);
|
||||||
|
if (!logStream) throw new BadRequestError({ message: "Audit log stream not found" });
|
||||||
|
|
||||||
|
const { orgId } = logStream;
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
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, headers };
|
||||||
|
};
|
||||||
|
|
||||||
|
const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListAuditLogStreamDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||||
|
|
||||||
|
const logStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
||||||
|
return logStreams;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
create,
|
||||||
|
updateById,
|
||||||
|
deleteById,
|
||||||
|
getById,
|
||||||
|
list
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type LogStreamHeaders = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||||
|
url: string;
|
||||||
|
headers?: LogStreamHeaders[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||||
|
id: string;
|
||||||
|
url?: string;
|
||||||
|
headers?: LogStreamHeaders[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListAuditLogStreamDTO = Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TGetDetailsAuditLogStreamDTO = Omit<TOrgPermission, "orgId"> & {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
|
import { RawAxiosRequestHeaders } from "axios";
|
||||||
|
|
||||||
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
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 { 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";
|
||||||
|
|
||||||
type TAuditLogQueueServiceFactoryDep = {
|
type TAuditLogQueueServiceFactoryDep = {
|
||||||
auditLogDAL: TAuditLogDALFactory;
|
auditLogDAL: TAuditLogDALFactory;
|
||||||
|
auditLogStreamDAL: Pick<TAuditLogStreamDALFactory, "find">;
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
@@ -15,11 +23,15 @@ type TAuditLogQueueServiceFactoryDep = {
|
|||||||
|
|
||||||
export type TAuditLogQueueServiceFactory = ReturnType<typeof auditLogQueueServiceFactory>;
|
export type TAuditLogQueueServiceFactory = ReturnType<typeof auditLogQueueServiceFactory>;
|
||||||
|
|
||||||
|
// keep this timeout 5s it must be fast because else the queue will take time to finish
|
||||||
|
// audit log is a crowded queue thus needs to be fast
|
||||||
|
export const AUDIT_LOG_STREAM_TIMEOUT = 5 * 1000;
|
||||||
export const auditLogQueueServiceFactory = ({
|
export const auditLogQueueServiceFactory = ({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
auditLogStreamDAL
|
||||||
}: TAuditLogQueueServiceFactoryDep) => {
|
}: TAuditLogQueueServiceFactoryDep) => {
|
||||||
const pushToLog = async (data: TCreateAuditLogDTO) => {
|
const pushToLog = async (data: TCreateAuditLogDTO) => {
|
||||||
await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, {
|
await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, {
|
||||||
@@ -47,7 +59,7 @@ export const auditLogQueueServiceFactory = ({
|
|||||||
// skip inserting if audit log retention is 0 meaning its not supported
|
// skip inserting if audit log retention is 0 meaning its not supported
|
||||||
if (ttl === 0) return;
|
if (ttl === 0) return;
|
||||||
|
|
||||||
await auditLogDAL.create({
|
const auditLog = await auditLogDAL.create({
|
||||||
actor: actor.type,
|
actor: actor.type,
|
||||||
actorMetadata: actor.metadata,
|
actorMetadata: actor.metadata,
|
||||||
userAgent,
|
userAgent,
|
||||||
@@ -59,6 +71,46 @@ export const auditLogQueueServiceFactory = ({
|
|||||||
eventMetadata: event.metadata,
|
eventMetadata: event.metadata,
|
||||||
userAgentType
|
userAgentType
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const logStreams = orgId ? await auditLogStreamDAL.find({ orgId }) : [];
|
||||||
|
await Promise.allSettled(
|
||||||
|
logStreams.map(
|
||||||
|
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 (streamHeaders.length)
|
||||||
|
streamHeaders.forEach(({ key, value }) => {
|
||||||
|
headers[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return request.post(url, auditLog, {
|
||||||
|
headers,
|
||||||
|
// request timeout
|
||||||
|
timeout: AUDIT_LOG_STREAM_TIMEOUT,
|
||||||
|
// connection timeout
|
||||||
|
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
queueService.start(QueueName.AuditLogPrune, async () => {
|
queueService.start(QueueName.AuditLogPrune, async () => {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
customAlerts: false,
|
customAlerts: false,
|
||||||
auditLogs: false,
|
auditLogs: false,
|
||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 0,
|
||||||
|
auditLogStreams: false,
|
||||||
|
auditLogStreamLimit: 3,
|
||||||
samlSSO: false,
|
samlSSO: false,
|
||||||
scim: false,
|
scim: false,
|
||||||
ldap: false,
|
ldap: false,
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export type TFeatureSet = {
|
|||||||
customAlerts: false;
|
customAlerts: false;
|
||||||
auditLogs: false;
|
auditLogs: false;
|
||||||
auditLogsRetentionDays: 0;
|
auditLogsRetentionDays: 0;
|
||||||
|
auditLogStreams: false;
|
||||||
|
auditLogStreamLimit: 3;
|
||||||
samlSSO: false;
|
samlSSO: false;
|
||||||
scim: false;
|
scim: false;
|
||||||
ldap: false;
|
ldap: false;
|
||||||
|
|||||||
@@ -614,3 +614,29 @@ export const INTEGRATION = {
|
|||||||
integrationId: "The ID of the integration object."
|
integrationId: "The ID of the integration object."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AUDIT_LOG_STREAMS = {
|
||||||
|
CREATE: {
|
||||||
|
url: "The HTTP URL to push logs to.",
|
||||||
|
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.",
|
||||||
|
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."
|
||||||
|
},
|
||||||
|
GET_BY_ID: {
|
||||||
|
id: "The ID of the audit log stream to get details."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ const envSchema = z
|
|||||||
})
|
})
|
||||||
.transform((data) => ({
|
.transform((data) => ({
|
||||||
...data,
|
...data,
|
||||||
|
isCloud: Boolean(data.LICENSE_SERVER_KEY),
|
||||||
isSmtpConfigured: Boolean(data.SMTP_HOST),
|
isSmtpConfigured: Boolean(data.SMTP_HOST),
|
||||||
isRedisConfigured: Boolean(data.REDIS_URL),
|
isRedisConfigured: Boolean(data.REDIS_URL),
|
||||||
isDevelopmentMode: data.NODE_ENV === "development",
|
isDevelopmentMode: data.NODE_ENV === "development",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export type TOrgPermission = {
|
|||||||
actorId: string;
|
actorId: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
actorOrgId: string | undefined;
|
actorOrgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProjectPermission = {
|
export type TProjectPermission = {
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export { isDisposableEmail } from "./validate-email";
|
export { isDisposableEmail } from "./validate-email";
|
||||||
|
export { validateLocalIps } from "./validate-url";
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { getConfig } from "../config/env";
|
||||||
|
import { BadRequestError } from "../errors";
|
||||||
|
|
||||||
|
export const validateLocalIps = (url: string) => {
|
||||||
|
const validUrl = new URL(url);
|
||||||
|
const appCfg = getConfig();
|
||||||
|
// on cloud local ips are not allowed
|
||||||
|
if (
|
||||||
|
appCfg.isCloud &&
|
||||||
|
(validUrl.host === "host.docker.internal" ||
|
||||||
|
validUrl.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||||
|
validUrl.host.match(/^192\.168\.\d+\.\d+/))
|
||||||
|
)
|
||||||
|
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||||
|
|
||||||
|
if (validUrl.host === "localhost" || validUrl.host === "127.0.0.1")
|
||||||
|
throw new BadRequestError({ message: "Localhost not allowed" });
|
||||||
|
};
|
||||||
@@ -5,6 +5,8 @@ import { registerV1EERoutes } from "@app/ee/routes/v1";
|
|||||||
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
||||||
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
|
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
|
||||||
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
|
import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-dal";
|
||||||
|
import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
|
||||||
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
|
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
|
||||||
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||||
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
|
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
|
||||||
@@ -193,6 +195,7 @@ export const registerRoutes = async (
|
|||||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||||
|
|
||||||
const auditLogDAL = auditLogDALFactory(db);
|
const auditLogDAL = auditLogDALFactory(db);
|
||||||
|
const auditLogStreamDAL = auditLogStreamDALFactory(db);
|
||||||
const trustedIpDAL = trustedIpDALFactory(db);
|
const trustedIpDAL = trustedIpDALFactory(db);
|
||||||
const telemetryDAL = telemetryDALFactory(db);
|
const telemetryDAL = telemetryDALFactory(db);
|
||||||
|
|
||||||
@@ -243,9 +246,15 @@ export const registerRoutes = async (
|
|||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
auditLogStreamDAL
|
||||||
});
|
});
|
||||||
const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue });
|
const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue });
|
||||||
|
const auditLogStreamService = auditLogStreamServiceFactory({
|
||||||
|
licenseService,
|
||||||
|
permissionService,
|
||||||
|
auditLogStreamDAL
|
||||||
|
});
|
||||||
const sapService = secretApprovalPolicyServiceFactory({
|
const sapService = secretApprovalPolicyServiceFactory({
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
@@ -715,6 +724,7 @@ export const registerRoutes = async (
|
|||||||
saml: samlService,
|
saml: samlService,
|
||||||
ldap: ldapService,
|
ldap: ldapService,
|
||||||
auditLog: auditLogService,
|
auditLog: auditLogService,
|
||||||
|
auditLogStream: auditLogStreamService,
|
||||||
secretScanning: secretScanningService,
|
secretScanning: secretScanningService,
|
||||||
license: licenseService,
|
license: licenseService,
|
||||||
trustedIp: trustedIpService,
|
trustedIp: trustedIpService,
|
||||||
|
|||||||
@@ -69,3 +69,10 @@ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
|||||||
keyEncoding: true,
|
keyEncoding: true,
|
||||||
algorithm: true
|
algorithm: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const SanitizedAuditLogStreamSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
title: "Audit Log Streams"
|
||||||
|
description: "Learn how to stream Infisical Audit Logs to external logging providers."
|
||||||
|
---
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Audit log streams is a paid feature.
|
||||||
|
|
||||||
|
If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical,
|
||||||
|
then you should contact [email protected] to purchase an enterprise license to use it.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
Infisical Audit Log Streaming enables you to transmit your organization's Audit Logs to external logging providers for monitoring and analysis.
|
||||||
|
|
||||||
|
The logs are formatted in JSON, requiring your logging provider to support JSON-based log parsing.
|
||||||
|
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to Organization Settings in your sidebar." />
|
||||||
|
<Step title="Select Audit Log Streams Tab.">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Click on Create">
|
||||||
|

|
||||||
|
|
||||||
|
Provide the following values
|
||||||
|
<ParamField path="Endpoint URL" type="string" required>
|
||||||
|
The HTTPS endpoint URL of the logging provider that collects the JSON stream.
|
||||||
|
</ParamField>
|
||||||
|
<ParamField path="Headers" type="string" >
|
||||||
|
The HTTP headers for the logging provider for identification and authentication.
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|

|
||||||
|
Your Audit Logs are now ready to be streamed.
|
||||||
|
|
||||||
|
## Example Providers
|
||||||
|
|
||||||
|
### Better Stack
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Select Connect Source">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Provide a name and select platform"/>
|
||||||
|
<Step title="Provide Audit Log Stream inputs">
|
||||||
|

|
||||||
|
|
||||||
|
1. Copy the **endpoint** from Better Stack to the **Endpoint URL** field.
|
||||||
|
3. Create a new header with key **Authorization** and set the value as **Bearer \<source token from betterstack\>**.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
### Datadog
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to API Keys section">
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Select New Key and provide a key name">
|
||||||
|

|
||||||
|

|
||||||
|
</Step>
|
||||||
|
<Step title="Find your Datadog region specific logging endpoint.">
|
||||||
|

|
||||||
|
|
||||||
|
1. Navigate to the [Datadog Send Logs API documentation](https://docs.datadoghq.com/api/latest/logs/?code-lang=curl&site=us5#send-logs).
|
||||||
|
2. Pick your Datadog account region.
|
||||||
|
3. Obtain your Datadog logging endpoint URL.
|
||||||
|
</Step>
|
||||||
|
<Step title="Provide audit log stream inputs">
|
||||||
|

|
||||||
|
|
||||||
|
1. Copy the **logging endpoint** from Datadog to the **Endpoint URL** field.
|
||||||
|
2. Copy the **API Key** from previous step
|
||||||
|
3. Create a new header with key **DD-API-KEY** and set the value as **API Key**.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 112 KiB |
@@ -143,7 +143,8 @@
|
|||||||
"documentation/platform/dynamic-secrets/aws-iam"
|
"documentation/platform/dynamic-secrets/aws-iam"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"documentation/platform/groups"
|
"documentation/platform/groups",
|
||||||
|
"documentation/platform/audit-log-streams"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export {
|
||||||
|
useCreateAuditLogStream,
|
||||||
|
useDeleteAuditLogStream,
|
||||||
|
useUpdateAuditLogStream
|
||||||
|
} from "./mutations";
|
||||||
|
export { useGetAuditLogStreamDetails, useGetAuditLogStreams } from "./queries";
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { auditLogStreamKeys } from "./queries";
|
||||||
|
import {
|
||||||
|
TAuditLogStream,
|
||||||
|
TCreateAuditLogStreamDTO,
|
||||||
|
TDeleteAuditLogStreamDTO,
|
||||||
|
TUpdateAuditLogStreamDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export const useCreateAuditLogStream = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{ auditLogStream: TAuditLogStream }, {}, TCreateAuditLogStreamDTO>({
|
||||||
|
mutationFn: async (dto) => {
|
||||||
|
const { data } = await apiRequest.post<{ auditLogStream: TAuditLogStream }>(
|
||||||
|
"/api/v1/audit-log-streams",
|
||||||
|
dto
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { orgId }) => {
|
||||||
|
queryClient.invalidateQueries(auditLogStreamKeys.list(orgId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateAuditLogStream = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{ auditLogStream: TAuditLogStream }, {}, TUpdateAuditLogStreamDTO>({
|
||||||
|
mutationFn: async (dto) => {
|
||||||
|
const { data } = await apiRequest.patch<{ auditLogStream: TAuditLogStream }>(
|
||||||
|
`/api/v1/audit-log-streams/${dto.id}`,
|
||||||
|
dto
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { orgId }) => {
|
||||||
|
queryClient.invalidateQueries(auditLogStreamKeys.list(orgId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteAuditLogStream = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{ auditLogStream: TAuditLogStream }, {}, TDeleteAuditLogStreamDTO>({
|
||||||
|
mutationFn: async (dto) => {
|
||||||
|
const { data } = await apiRequest.delete<{ auditLogStream: TAuditLogStream }>(
|
||||||
|
`/api/v1/audit-log-streams/${dto.id}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { orgId }) => {
|
||||||
|
queryClient.invalidateQueries(auditLogStreamKeys.list(orgId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { TAuditLogStream } from "./types";
|
||||||
|
|
||||||
|
export const auditLogStreamKeys = {
|
||||||
|
list: (orgId: string) => ["audit-log-stream", { orgId }],
|
||||||
|
getById: (id: string) => ["audit-log-stream-details", { id }]
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAuditLogStreams = async () => {
|
||||||
|
const { data } = await apiRequest.get<{ auditLogStreams: TAuditLogStream[] }>(
|
||||||
|
"/api/v1/audit-log-streams"
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.auditLogStreams;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetAuditLogStreams = (orgId: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: auditLogStreamKeys.list(orgId),
|
||||||
|
queryFn: () => fetchAuditLogStreams(),
|
||||||
|
enabled: Boolean(orgId)
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchAuditLogStreamDetails = async (id: string) => {
|
||||||
|
const { data } = await apiRequest.get<{ auditLogStream: TAuditLogStream }>(
|
||||||
|
`/api/v1/audit-log-streams/${id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.auditLogStream;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetAuditLogStreamDetails = (id: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: auditLogStreamKeys.getById(id),
|
||||||
|
queryFn: () => fetchAuditLogStreamDetails(id),
|
||||||
|
enabled: Boolean(id)
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export type LogStreamHeaders = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAuditLogStream = {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
headers?: LogStreamHeaders[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateAuditLogStreamDTO = {
|
||||||
|
url: string;
|
||||||
|
headers?: LogStreamHeaders[];
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateAuditLogStreamDTO = {
|
||||||
|
id: string;
|
||||||
|
url?: string;
|
||||||
|
headers?: LogStreamHeaders[];
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteAuditLogStreamDTO = {
|
||||||
|
id: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from "./admin";
|
export * from "./admin";
|
||||||
export * from "./apiKeys";
|
export * from "./apiKeys";
|
||||||
export * from "./auditLogs";
|
export * from "./auditLogs";
|
||||||
|
export * from "./auditLogStreams";
|
||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
export * from "./bots";
|
export * from "./bots";
|
||||||
export * from "./dynamicSecret";
|
export * from "./dynamicSecret";
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export type SubscriptionPlan = {
|
|||||||
auditLogs: boolean;
|
auditLogs: boolean;
|
||||||
dynamicSecret: boolean;
|
dynamicSecret: boolean;
|
||||||
auditLogsRetentionDays: number;
|
auditLogsRetentionDays: number;
|
||||||
|
auditLogStreamLimit: number;
|
||||||
|
auditLogStreams: boolean;
|
||||||
customAlerts: boolean;
|
customAlerts: boolean;
|
||||||
customRateLimits: boolean;
|
customRateLimits: boolean;
|
||||||
pitRecovery: boolean;
|
pitRecovery: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ZodIssue } from "zod";
|
import { ZodIssue } from "zod";
|
||||||
|
|
||||||
|
export type { TAuditLogStream } from "./auditLogStreams/types";
|
||||||
export type { GetAuthTokenAPI } from "./auth/types";
|
export type { GetAuthTokenAPI } from "./auth/types";
|
||||||
export type { IncidentContact } from "./incidentContacts/types";
|
export type { IncidentContact } from "./incidentContacts/types";
|
||||||
export type { IntegrationAuth } from "./integrationAuth/types";
|
export type { IntegrationAuth } from "./integrationAuth/types";
|
||||||
@@ -48,13 +49,13 @@ export enum ApiErrorTypes {
|
|||||||
|
|
||||||
export type TApiErrors =
|
export type TApiErrors =
|
||||||
| {
|
| {
|
||||||
error: ApiErrorTypes.ValidationError;
|
error: ApiErrorTypes.ValidationError;
|
||||||
message: ZodIssue[];
|
message: ZodIssue[];
|
||||||
statusCode: 403;
|
statusCode: 403;
|
||||||
}
|
}
|
||||||
| { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 }
|
| { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 }
|
||||||
| {
|
| {
|
||||||
statusCode: 400;
|
statusCode: 400;
|
||||||
message: string;
|
message: string;
|
||||||
error: ApiErrorTypes.BadRequestError;
|
error: ApiErrorTypes.BadRequestError;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
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, FormLabel, IconButton, Input, Spinner } from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import {
|
||||||
|
useCreateAuditLogStream,
|
||||||
|
useGetAuditLogStreamDetails,
|
||||||
|
useUpdateAuditLogStream
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id?: string;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
url: z.string().url().min(1),
|
||||||
|
headers: z
|
||||||
|
.object({
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const AuditLogStreamForm = ({ id = "", onClose }: Props) => {
|
||||||
|
const isEdit = Boolean(id);
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const orgId = currentOrg?.id || "";
|
||||||
|
|
||||||
|
const auditLogStream = useGetAuditLogStreamDetails(id);
|
||||||
|
const createAuditLogStream = useCreateAuditLogStream();
|
||||||
|
const updateAuditLogStream = useUpdateAuditLogStream();
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
setValue,
|
||||||
|
getValues,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TForm>({
|
||||||
|
values: auditLogStream?.data,
|
||||||
|
defaultValues: {
|
||||||
|
headers: [{ key: "", value: "" }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerFields = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "headers"
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAuditLogStreamEdit = async ({ headers, url }: TForm) => {
|
||||||
|
if (!id) return;
|
||||||
|
try {
|
||||||
|
await updateAuditLogStream.mutateAsync({
|
||||||
|
id,
|
||||||
|
orgId,
|
||||||
|
headers,
|
||||||
|
url
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully updated stream"
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update stream"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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({ headers: streamHeaders, url });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await createAuditLogStream.mutateAsync({
|
||||||
|
orgId,
|
||||||
|
headers: streamHeaders,
|
||||||
|
url
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully created stream"
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create stream"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEdit && auditLogStream.isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center p-8">
|
||||||
|
<Spinner size="lg" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleFormSubmit)} autoComplete="off">
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="url"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Endpoint URL"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormLabel label="Headers" isOptional />
|
||||||
|
{headerFields.fields.map(({ id: headerFieldId }, i) => (
|
||||||
|
<div key={headerFieldId} className="flex space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`headers.${i}.key`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
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: "" }]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => headerFields.append({ value: "", key: "" })}
|
||||||
|
>
|
||||||
|
Add Key
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button className="mr-4" type="submit" isLoading={isSubmitting}>
|
||||||
|
{isEdit ? "Save" : "Create"}
|
||||||
|
</Button>
|
||||||
|
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import { faPlug, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
THead,
|
||||||
|
Tr,
|
||||||
|
UpgradePlanModal
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects,
|
||||||
|
useOrganization,
|
||||||
|
useSubscription
|
||||||
|
} from "@app/context";
|
||||||
|
import { withPermission } from "@app/hoc";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useDeleteAuditLogStream, useGetAuditLogStreams } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { AuditLogStreamForm } from "./AuditLogStreamForm";
|
||||||
|
|
||||||
|
export const AuditLogStreamsTab = withPermission(
|
||||||
|
() => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const orgId = currentOrg?.id || "";
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
|
"auditLogStreamForm",
|
||||||
|
"deleteAuditLogStream",
|
||||||
|
"upgradePlan"
|
||||||
|
] as const);
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
|
const { data: auditLogStreams, isLoading: isAuditLogStreamsLoading } =
|
||||||
|
useGetAuditLogStreams(orgId);
|
||||||
|
|
||||||
|
// mutation
|
||||||
|
const { mutateAsync: deleteAuditLogStream } = useDeleteAuditLogStream();
|
||||||
|
|
||||||
|
const handleAuditLogStreamDelete = async () => {
|
||||||
|
try {
|
||||||
|
const auditLogStreamId = popUp?.deleteAuditLogStream?.data as string;
|
||||||
|
await deleteAuditLogStream({
|
||||||
|
id: auditLogStreamId,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
handlePopUpClose("deleteAuditLogStream");
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully deleted stream"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to delete stream"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Audit Log Streams</p>
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Settings}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription && !subscription?.auditLogStreams) {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handlePopUpOpen("auditLogStreamForm");
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
<p className="mb-8 text-gray-400">
|
||||||
|
Send audit logs from Infisical to external logging providers via HTTP
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Td>URL</Td>
|
||||||
|
<Td className="text-right">Action</Td>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isAuditLogStreamsLoading && (
|
||||||
|
<TableSkeleton columns={2} innerKey="stream-loading" />
|
||||||
|
)}
|
||||||
|
{!isAuditLogStreamsLoading && auditLogStreams && auditLogStreams?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={5}>
|
||||||
|
<EmptyState title="No audit log streams found" icon={faPlug} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
{!isAuditLogStreamsLoading &&
|
||||||
|
auditLogStreams?.map(({ id, url }) => (
|
||||||
|
<Tr key={id}>
|
||||||
|
<Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
|
||||||
|
{url}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex items-center justify-end space-x-2">
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.Settings}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={() => handlePopUpOpen("auditLogStreamForm", id)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.Settings}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
className="border-red-800 bg-red-800 hover:border-red-700 hover:bg-red-700"
|
||||||
|
colorSchema="danger"
|
||||||
|
size="xs"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={() => handlePopUpOpen("deleteAuditLogStream", id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.auditLogStreamForm.isOpen}
|
||||||
|
onOpenChange={(isModalOpen) => {
|
||||||
|
handlePopUpToggle("auditLogStreamForm", isModalOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title={`${popUp?.auditLogStreamForm?.data ? "Update" : "Create"} Audit Log Stream `}
|
||||||
|
subTitle="Continuously stream logs from Infisical to third-party logging providers."
|
||||||
|
>
|
||||||
|
<AuditLogStreamForm
|
||||||
|
id={popUp?.auditLogStreamForm?.data as string}
|
||||||
|
onClose={() => handlePopUpToggle("auditLogStreamForm")}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<UpgradePlanModal
|
||||||
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
text="You can add audit log streams if you switch to Infisical's Enterprise plan."
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteAuditLogStream.isOpen}
|
||||||
|
deleteKey="delete"
|
||||||
|
title="Are you sure you want to remove this stream?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteAuditLogStream", isOpen)}
|
||||||
|
onClose={() => handlePopUpClose("deleteAuditLogStream")}
|
||||||
|
onDeleteApproved={handleAuditLogStreamDelete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Settings }
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { AuditLogStreamsTab } from "./AuditLogStreamTab";
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
|
|
||||||
|
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
||||||
import { OrgAuthTab } from "../OrgAuthTab";
|
import { OrgAuthTab } from "../OrgAuthTab";
|
||||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: "General", key: "tab-org-general" },
|
{ name: "General", key: "tab-org-general" },
|
||||||
{ name: "Security", key: "tab-org-security" }
|
{ name: "Security", key: "tab-org-security" },
|
||||||
|
{ name: "Audit Log Streams", key: "tag-audit-log-streams" }
|
||||||
];
|
];
|
||||||
export const OrgTabGroup = () => {
|
export const OrgTabGroup = () => {
|
||||||
return (
|
return (
|
||||||
@@ -17,9 +19,8 @@ export const OrgTabGroup = () => {
|
|||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
|
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
|
||||||
selected ? "border-b border-white text-white" : "text-mineshaft-400"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{tab.name}
|
{tab.name}
|
||||||
</button>
|
</button>
|
||||||
@@ -34,6 +35,9 @@ export const OrgTabGroup = () => {
|
|||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
<OrgAuthTab />
|
<OrgAuthTab />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>
|
||||||
|
<AuditLogStreamsTab />
|
||||||
|
</Tab.Panel>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { WebhooksTab } from "./components/WebhooksTab";
|
|||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: "General", key: "tab-project-general" },
|
{ name: "General", key: "tab-project-general" },
|
||||||
{ name: "Webhooks", key: "tab-project-webhooks" }
|
{ name: "Webhooks", key: "tab-project-webhooks" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ProjectSettingsPage = () => {
|
export const ProjectSettingsPage = () => {
|
||||||
@@ -25,9 +25,8 @@ export const ProjectSettingsPage = () => {
|
|||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
|
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
|
||||||
selected ? "border-b border-white text-white" : "text-mineshaft-400"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{tab.name}
|
{tab.name}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||