mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-27 15:26:41 +00:00
Add action audit middleware and tables
This commit is contained in:
@@ -6,7 +6,8 @@ import {
|
|||||||
integer,
|
integer,
|
||||||
bigint,
|
bigint,
|
||||||
real,
|
real,
|
||||||
text
|
text,
|
||||||
|
index
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
|
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
|
||||||
@@ -213,6 +214,22 @@ export const sessionTransferToken = pgTable("sessionTransferToken", {
|
|||||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionAuditLog = pgTable("actionAuditLog", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
actorType: varchar("actorType", { length: 50 }).notNull(),
|
||||||
|
actor: varchar("actor", { length: 255 }).notNull(),
|
||||||
|
actorId: varchar("actorId", { length: 255 }).notNull(),
|
||||||
|
action: varchar("action", { length: 100 }).notNull(),
|
||||||
|
metadata: text("metadata")
|
||||||
|
}, (table) => ([
|
||||||
|
index("idx_actionAuditLog_timestamp").on(table.timestamp),
|
||||||
|
index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
||||||
|
]));
|
||||||
|
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
export type Certificate = InferSelectModel<typeof certificates>;
|
export type Certificate = InferSelectModel<typeof certificates>;
|
||||||
@@ -230,3 +247,4 @@ export type RemoteExitNodeSession = InferSelectModel<
|
|||||||
>;
|
>;
|
||||||
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
|
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
|
||||||
export type LoginPage = InferSelectModel<typeof loginPage>;
|
export type LoginPage = InferSelectModel<typeof loginPage>;
|
||||||
|
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
||||||
@@ -2,10 +2,12 @@ import {
|
|||||||
sqliteTable,
|
sqliteTable,
|
||||||
integer,
|
integer,
|
||||||
text,
|
text,
|
||||||
real
|
real,
|
||||||
|
index
|
||||||
} from "drizzle-orm/sqlite-core";
|
} from "drizzle-orm/sqlite-core";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
|
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
|
||||||
|
import { metadata } from "@app/app/[orgId]/settings/layout";
|
||||||
|
|
||||||
export const certificates = sqliteTable("certificates", {
|
export const certificates = sqliteTable("certificates", {
|
||||||
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
||||||
@@ -207,6 +209,22 @@ export const sessionTransferToken = sqliteTable("sessionTransferToken", {
|
|||||||
expiresAt: integer("expiresAt").notNull()
|
expiresAt: integer("expiresAt").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionAuditLog = sqliteTable("actionAuditLog", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||||
|
orgId: text("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
actorType: text("actorType").notNull(),
|
||||||
|
actor: text("actor").notNull(),
|
||||||
|
actorId: text("actorId").notNull(),
|
||||||
|
action: text("action").notNull(),
|
||||||
|
metadata: text("metadata")
|
||||||
|
}, (table) => ([
|
||||||
|
index("idx_actionAuditLog_timestamp").on(table.timestamp),
|
||||||
|
index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
||||||
|
]));
|
||||||
|
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
export type Certificate = InferSelectModel<typeof certificates>;
|
export type Certificate = InferSelectModel<typeof certificates>;
|
||||||
@@ -224,3 +242,4 @@ export type RemoteExitNodeSession = InferSelectModel<
|
|||||||
>;
|
>;
|
||||||
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
|
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
|
||||||
export type LoginPage = InferSelectModel<typeof loginPage>;
|
export type LoginPage = InferSelectModel<typeof loginPage>;
|
||||||
|
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
||||||
@@ -15,4 +15,4 @@ export * from "./verifyCertificateAccess";
|
|||||||
export * from "./verifyRemoteExitNodeAccess";
|
export * from "./verifyRemoteExitNodeAccess";
|
||||||
export * from "./verifyIdpAccess";
|
export * from "./verifyIdpAccess";
|
||||||
export * from "./verifyLoginPageAccess";
|
export * from "./verifyLoginPageAccess";
|
||||||
export * from "../../lib/corsWithLoginPage";
|
export * from "./logActionAudit";
|
||||||
88
server/private/middlewares/logActionAudit.ts
Normal file
88
server/private/middlewares/logActionAudit.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
|
import { actionAuditLog, db } from "@server/db";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
|
||||||
|
export function logActionAudit(action: ActionsEnum) {
|
||||||
|
return async function (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
let orgId;
|
||||||
|
let actorType;
|
||||||
|
let actor;
|
||||||
|
let actorId;
|
||||||
|
|
||||||
|
const user = req.user;
|
||||||
|
if (user) {
|
||||||
|
const userOrg = req.userOrg;
|
||||||
|
orgId = userOrg?.orgId;
|
||||||
|
actorType = "user";
|
||||||
|
actor = user.username;
|
||||||
|
actorId = user.userId;
|
||||||
|
}
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
if (apiKey) {
|
||||||
|
const apiKeyOrg = req.apiKeyOrg;
|
||||||
|
orgId = apiKeyOrg?.orgId;
|
||||||
|
actorType = "apiKey";
|
||||||
|
actor = apiKey.name;
|
||||||
|
actorId = apiKey.apiKeyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
logger.warn("logActionAudit: No organization context found");
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!actorType || !actor || !actorId) {
|
||||||
|
logger.warn("logActionAudit: Incomplete actor information");
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
let metadata = null;
|
||||||
|
if (req.params) {
|
||||||
|
metadata = JSON.stringify(req.params);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(actionAuditLog).values({
|
||||||
|
timestamp,
|
||||||
|
orgId,
|
||||||
|
actorType,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
action,
|
||||||
|
metadata
|
||||||
|
});
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying logging action"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user