mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-10 20:02:26 +00:00
Merge branch 'dev' into user-compliance
This commit is contained in:
68
server/routers/auditLogs/exportRequstAuditLog.ts
Normal file
68
server/routers/auditLogs/exportRequstAuditLog.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { registry } from "@server/openApi";
|
||||
import { NextFunction } from "express";
|
||||
import { Request, Response } from "express";
|
||||
import { OpenAPITags } from "@server/openApi";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { queryAccessAuditLogsQuery, queryRequestAuditLogsParams, queryRequest } from "./queryRequstAuditLog";
|
||||
import { generateCSV } from "./generateCSV";
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/logs/request",
|
||||
description: "Query the request audit log for an organization",
|
||||
tags: [OpenAPITags.Org],
|
||||
request: {
|
||||
query: queryAccessAuditLogsQuery,
|
||||
params: queryRequestAuditLogsParams
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function exportRequestAuditLogs(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = queryAccessAuditLogsQuery.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedQuery.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedParams = queryRequestAuditLogsParams.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data = { ...parsedQuery.data, ...parsedParams.data };
|
||||
|
||||
const baseQuery = queryRequest(data);
|
||||
|
||||
const log = await baseQuery.limit(data.limit).offset(data.offset);
|
||||
|
||||
const csvData = generateCSV(log);
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="request-audit-logs-${data.orgId}-${Date.now()}.csv"`);
|
||||
|
||||
return res.send(csvData);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
16
server/routers/auditLogs/generateCSV.ts
Normal file
16
server/routers/auditLogs/generateCSV.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export function generateCSV(data: any[]): string {
|
||||
if (data.length === 0) {
|
||||
return "orgId,action,actorType,timestamp,actor\n";
|
||||
}
|
||||
|
||||
const headers = Object.keys(data[0]).join(",");
|
||||
const rows = data.map(row =>
|
||||
Object.values(row).map(value =>
|
||||
typeof value === 'string' && value.includes(',')
|
||||
? `"${value.replace(/"/g, '""')}"`
|
||||
: value
|
||||
).join(",")
|
||||
);
|
||||
|
||||
return [headers, ...rows].join("\n");
|
||||
}
|
||||
2
server/routers/auditLogs/index.ts
Normal file
2
server/routers/auditLogs/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./queryRequstAuditLog";
|
||||
export * from "./exportRequstAuditLog";
|
||||
276
server/routers/auditLogs/queryRequstAuditLog.ts
Normal file
276
server/routers/auditLogs/queryRequstAuditLog.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { db, requestAuditLog, resources } from "@server/db";
|
||||
import { registry } from "@server/openApi";
|
||||
import { NextFunction } from "express";
|
||||
import { Request, Response } from "express";
|
||||
import { eq, gt, lt, and, count } from "drizzle-orm";
|
||||
import { OpenAPITags } from "@server/openApi";
|
||||
import { z } from "zod";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export const queryAccessAuditLogsQuery = z.object({
|
||||
// iso string just validate its a parseable date
|
||||
timeStart: z
|
||||
.string()
|
||||
.refine((val) => !isNaN(Date.parse(val)), {
|
||||
message: "timeStart must be a valid ISO date string"
|
||||
})
|
||||
.transform((val) => Math.floor(new Date(val).getTime() / 1000)),
|
||||
timeEnd: z
|
||||
.string()
|
||||
.refine((val) => !isNaN(Date.parse(val)), {
|
||||
message: "timeEnd must be a valid ISO date string"
|
||||
})
|
||||
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
|
||||
.optional()
|
||||
.default(new Date().toISOString()),
|
||||
action: z
|
||||
.union([z.boolean(), z.string()])
|
||||
.transform((val) => (typeof val === "string" ? val === "true" : val))
|
||||
.optional(),
|
||||
method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional(),
|
||||
reason: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
.optional(),
|
||||
resourceId: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
.optional(),
|
||||
actor: z.string().optional(),
|
||||
location: z.string().optional(),
|
||||
host: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
limit: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("1000")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive()),
|
||||
offset: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("0")
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().nonnegative())
|
||||
});
|
||||
|
||||
export const queryRequestAuditLogsParams = z.object({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
export const queryRequestAuditLogsCombined =
|
||||
queryAccessAuditLogsQuery.merge(queryRequestAuditLogsParams);
|
||||
type Q = z.infer<typeof queryRequestAuditLogsCombined>;
|
||||
|
||||
function getWhere(data: Q) {
|
||||
return and(
|
||||
gt(requestAuditLog.timestamp, data.timeStart),
|
||||
lt(requestAuditLog.timestamp, data.timeEnd),
|
||||
eq(requestAuditLog.orgId, data.orgId),
|
||||
data.resourceId
|
||||
? eq(requestAuditLog.resourceId, data.resourceId)
|
||||
: undefined,
|
||||
data.actor ? eq(requestAuditLog.actor, data.actor) : undefined,
|
||||
data.method ? eq(requestAuditLog.method, data.method) : undefined,
|
||||
data.reason ? eq(requestAuditLog.reason, data.reason) : undefined,
|
||||
data.host ? eq(requestAuditLog.host, data.host) : undefined,
|
||||
data.location ? eq(requestAuditLog.location, data.location) : undefined,
|
||||
data.path ? eq(requestAuditLog.path, data.path) : undefined,
|
||||
data.action !== undefined
|
||||
? eq(requestAuditLog.action, data.action)
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
export function queryRequest(data: Q) {
|
||||
return db
|
||||
.select({
|
||||
timestamp: requestAuditLog.timestamp,
|
||||
orgId: requestAuditLog.orgId,
|
||||
action: requestAuditLog.action,
|
||||
reason: requestAuditLog.reason,
|
||||
actorType: requestAuditLog.actorType,
|
||||
actor: requestAuditLog.actor,
|
||||
actorId: requestAuditLog.actorId,
|
||||
resourceId: requestAuditLog.resourceId,
|
||||
ip: requestAuditLog.ip,
|
||||
location: requestAuditLog.location,
|
||||
userAgent: requestAuditLog.userAgent,
|
||||
metadata: requestAuditLog.metadata,
|
||||
headers: requestAuditLog.headers,
|
||||
query: requestAuditLog.query,
|
||||
originalRequestURL: requestAuditLog.originalRequestURL,
|
||||
scheme: requestAuditLog.scheme,
|
||||
host: requestAuditLog.host,
|
||||
path: requestAuditLog.path,
|
||||
method: requestAuditLog.method,
|
||||
tls: requestAuditLog.tls,
|
||||
resourceName: resources.name,
|
||||
resourceNiceId: resources.niceId
|
||||
})
|
||||
.from(requestAuditLog)
|
||||
.leftJoin(
|
||||
resources,
|
||||
eq(requestAuditLog.resourceId, resources.resourceId)
|
||||
) // TODO: Is this efficient?
|
||||
.where(getWhere(data))
|
||||
.orderBy(requestAuditLog.timestamp);
|
||||
}
|
||||
|
||||
export function countRequestQuery(data: Q) {
|
||||
const countQuery = db
|
||||
.select({ count: count() })
|
||||
.from(requestAuditLog)
|
||||
.where(getWhere(data));
|
||||
return countQuery;
|
||||
}
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/logs/request",
|
||||
description: "Query the request audit log for an organization",
|
||||
tags: [OpenAPITags.Org],
|
||||
request: {
|
||||
query: queryAccessAuditLogsQuery,
|
||||
params: queryRequestAuditLogsParams
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
async function queryUniqueFilterAttributes(
|
||||
timeStart: number,
|
||||
timeEnd: number,
|
||||
orgId: string
|
||||
) {
|
||||
const baseConditions = and(
|
||||
gt(requestAuditLog.timestamp, timeStart),
|
||||
lt(requestAuditLog.timestamp, timeEnd),
|
||||
eq(requestAuditLog.orgId, orgId)
|
||||
);
|
||||
|
||||
// Get unique actors
|
||||
const uniqueActors = await db
|
||||
.selectDistinct({
|
||||
actor: requestAuditLog.actor
|
||||
})
|
||||
.from(requestAuditLog)
|
||||
.where(baseConditions);
|
||||
|
||||
// Get unique locations
|
||||
const uniqueLocations = await db
|
||||
.selectDistinct({
|
||||
locations: requestAuditLog.location
|
||||
})
|
||||
.from(requestAuditLog)
|
||||
.where(baseConditions);
|
||||
|
||||
// Get unique actors
|
||||
const uniqueHosts = await db
|
||||
.selectDistinct({
|
||||
hosts: requestAuditLog.host
|
||||
})
|
||||
.from(requestAuditLog)
|
||||
.where(baseConditions);
|
||||
|
||||
// Get unique actors
|
||||
const uniquePaths = await db
|
||||
.selectDistinct({
|
||||
paths: requestAuditLog.path
|
||||
})
|
||||
.from(requestAuditLog)
|
||||
.where(baseConditions);
|
||||
|
||||
// Get unique resources with names
|
||||
const uniqueResources = await db
|
||||
.selectDistinct({
|
||||
id: requestAuditLog.resourceId,
|
||||
name: resources.name
|
||||
})
|
||||
.from(requestAuditLog)
|
||||
.leftJoin(
|
||||
resources,
|
||||
eq(requestAuditLog.resourceId, resources.resourceId)
|
||||
)
|
||||
.where(baseConditions);
|
||||
|
||||
return {
|
||||
actors: uniqueActors.map(row => row.actor).filter((actor): actor is string => actor !== null),
|
||||
resources: uniqueResources.filter((row): row is { id: number; name: string | null } => row.id !== null),
|
||||
locations: uniqueLocations.map(row => row.locations).filter((location): location is string => location !== null),
|
||||
hosts: uniqueHosts.map(row => row.hosts).filter((host): host is string => host !== null),
|
||||
paths: uniquePaths.map(row => row.paths).filter((path): path is string => path !== null)
|
||||
};
|
||||
}
|
||||
|
||||
export async function queryRequestAuditLogs(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = queryAccessAuditLogsQuery.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedQuery.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedParams = queryRequestAuditLogsParams.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const data = { ...parsedQuery.data, ...parsedParams.data };
|
||||
|
||||
const baseQuery = queryRequest(data);
|
||||
|
||||
const log = await baseQuery.limit(data.limit).offset(data.offset);
|
||||
|
||||
const totalCountResult = await countRequestQuery(data);
|
||||
const totalCount = totalCountResult[0].count;
|
||||
|
||||
const filterAttributes = await queryUniqueFilterAttributes(
|
||||
data.timeStart,
|
||||
data.timeEnd,
|
||||
data.orgId
|
||||
);
|
||||
|
||||
return response<QueryRequestAuditLogResponse>(res, {
|
||||
data: {
|
||||
log: log,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
limit: data.limit,
|
||||
offset: data.offset
|
||||
},
|
||||
filterAttributes
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Action audit logs retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
93
server/routers/auditLogs/types.ts
Normal file
93
server/routers/auditLogs/types.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
export type QueryActionAuditLogResponse = {
|
||||
log: {
|
||||
orgId: string;
|
||||
action: string;
|
||||
actorType: string;
|
||||
actorId: string;
|
||||
metadata: string | null;
|
||||
timestamp: number;
|
||||
actor: string;
|
||||
}[];
|
||||
pagination: {
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
filterAttributes: {
|
||||
actors: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type QueryRequestAuditLogResponse = {
|
||||
log: {
|
||||
timestamp: number;
|
||||
action: boolean;
|
||||
reason: number;
|
||||
orgId: string | null;
|
||||
actorType: string | null;
|
||||
actor: string | null;
|
||||
actorId: string | null;
|
||||
resourceId: number | null;
|
||||
resourceNiceId: string | null;
|
||||
resourceName: string | null;
|
||||
ip: string | null;
|
||||
location: string | null;
|
||||
userAgent: string | null;
|
||||
metadata: string | null;
|
||||
headers: string | null;
|
||||
query: string | null;
|
||||
originalRequestURL: string | null;
|
||||
scheme: string | null;
|
||||
host: string | null;
|
||||
path: string | null;
|
||||
method: string | null;
|
||||
tls: boolean | null;
|
||||
}[];
|
||||
pagination: {
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
filterAttributes: {
|
||||
actors: string[];
|
||||
resources: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
}[];
|
||||
locations: string[];
|
||||
hosts: string[];
|
||||
paths: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type QueryAccessAuditLogResponse = {
|
||||
log: {
|
||||
orgId: string;
|
||||
action: boolean;
|
||||
actorType: string | null;
|
||||
actorId: string | null;
|
||||
resourceId: number | null;
|
||||
resourceName: string | null;
|
||||
resourceNiceId: string | null;
|
||||
ip: string | null;
|
||||
location: string | null;
|
||||
userAgent: string | null;
|
||||
metadata: string | null;
|
||||
type: string;
|
||||
timestamp: number;
|
||||
actor: string | null;
|
||||
}[];
|
||||
pagination: {
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
filterAttributes: {
|
||||
actors: string[];
|
||||
resources: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
}[];
|
||||
locations: string[];
|
||||
};
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
generateSessionToken,
|
||||
serializeSessionCookie
|
||||
} from "@server/auth/sessions/app";
|
||||
import { db } from "@server/db";
|
||||
import { db, resources } from "@server/db";
|
||||
import { users, securityKeys } from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
@@ -18,12 +18,14 @@ import logger from "@server/logger";
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
import { verifySession } from "@server/auth/sessions/verifySession";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { logAccessAudit } from "@server/private/lib/logAccessAudit";
|
||||
|
||||
export const loginBodySchema = z
|
||||
.object({
|
||||
email: z.string().toLowerCase().email(),
|
||||
password: z.string(),
|
||||
code: z.string().optional()
|
||||
code: z.string().optional(),
|
||||
resourceGuid: z.string().optional()
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -52,7 +54,7 @@ export async function login(
|
||||
);
|
||||
}
|
||||
|
||||
const { email, password, code } = parsedBody.data;
|
||||
const { email, password, code, resourceGuid } = parsedBody.data;
|
||||
|
||||
try {
|
||||
const { session: existingSession } = await verifySession(req);
|
||||
@@ -66,6 +68,28 @@ export async function login(
|
||||
});
|
||||
}
|
||||
|
||||
let resourceId: number | null = null;
|
||||
let orgId: string | null = null;
|
||||
if (resourceGuid) {
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceGuid, resourceGuid))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with GUID ${resourceGuid} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
resourceId = resource.resourceId;
|
||||
orgId = resource.orgId;
|
||||
}
|
||||
|
||||
const existingUserRes = await db
|
||||
.select()
|
||||
.from(users)
|
||||
@@ -78,6 +102,18 @@ export async function login(
|
||||
`Username or password incorrect. Email: ${email}. IP: ${req.ip}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceId && orgId) {
|
||||
logAccessAudit({
|
||||
orgId: orgId,
|
||||
resourceId: resourceId,
|
||||
action: false,
|
||||
type: "login",
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
}
|
||||
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
@@ -98,6 +134,18 @@ export async function login(
|
||||
`Username or password incorrect. Email: ${email}. IP: ${req.ip}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceId && orgId) {
|
||||
logAccessAudit({
|
||||
orgId: orgId,
|
||||
resourceId: resourceId,
|
||||
action: false,
|
||||
type: "login",
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
}
|
||||
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
@@ -158,6 +206,18 @@ export async function login(
|
||||
`Two-factor code incorrect. Email: ${email}. IP: ${req.ip}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceId && orgId) {
|
||||
logAccessAudit({
|
||||
orgId: orgId,
|
||||
resourceId: resourceId,
|
||||
action: false,
|
||||
type: "login",
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
}
|
||||
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
|
||||
191
server/routers/badger/logRequestAudit.ts
Normal file
191
server/routers/badger/logRequestAudit.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { db, orgs, requestAuditLog } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { and, eq, lt } from "drizzle-orm";
|
||||
import cache from "@server/lib/cache";
|
||||
|
||||
/**
|
||||
|
||||
Reasons:
|
||||
100 - Allowed by Rule
|
||||
101 - Allowed No Auth
|
||||
102 - Valid Access Token
|
||||
103 - Valid header auth
|
||||
104 - Valid Pincode
|
||||
105 - Valid Password
|
||||
106 - Valid email
|
||||
107 - Valid SSO
|
||||
|
||||
201 - Resource Not Found
|
||||
202 - Resource Blocked
|
||||
203 - Dropped by Rule
|
||||
204 - No Sessions
|
||||
205 - Temporary Request Token
|
||||
299 - No More Auth Methods
|
||||
|
||||
*/
|
||||
|
||||
async function getRetentionDays(orgId: string): Promise<number> {
|
||||
// check cache first
|
||||
const cached = cache.get<number>(`org_${orgId}_retentionDays`);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const [org] = await db
|
||||
.select({
|
||||
settingsLogRetentionDaysRequest:
|
||||
orgs.settingsLogRetentionDaysRequest
|
||||
})
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.limit(1);
|
||||
|
||||
if (!org) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// store the result in cache
|
||||
cache.set(
|
||||
`org_${orgId}_retentionDays`,
|
||||
org.settingsLogRetentionDaysRequest,
|
||||
300
|
||||
);
|
||||
|
||||
return org.settingsLogRetentionDaysRequest;
|
||||
}
|
||||
|
||||
export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const cutoffTimestamp = now - retentionDays * 24 * 60 * 60;
|
||||
|
||||
try {
|
||||
const deleteResult = await db
|
||||
.delete(requestAuditLog)
|
||||
.where(
|
||||
and(
|
||||
lt(requestAuditLog.timestamp, cutoffTimestamp),
|
||||
eq(requestAuditLog.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`Cleaned up ${deleteResult.changes} request audit logs older than ${retentionDays} days`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error cleaning up old request audit logs:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logRequestAudit(
|
||||
data: {
|
||||
action: boolean;
|
||||
reason: number;
|
||||
resourceId?: number;
|
||||
orgId?: string;
|
||||
location?: string;
|
||||
user?: { username: string; userId: string };
|
||||
apiKey?: { name: string | null; apiKeyId: string };
|
||||
metadata?: any;
|
||||
// userAgent?: string;
|
||||
},
|
||||
body: {
|
||||
path: string;
|
||||
originalRequestURL: string;
|
||||
scheme: string;
|
||||
host: string;
|
||||
method: string;
|
||||
tls: boolean;
|
||||
sessions?: Record<string, string>;
|
||||
headers?: Record<string, string>;
|
||||
query?: Record<string, string>;
|
||||
requestIp?: string;
|
||||
}
|
||||
) {
|
||||
try {
|
||||
if (data.orgId) {
|
||||
const retentionDays = await getRetentionDays(data.orgId);
|
||||
if (retentionDays === 0) {
|
||||
// do not log
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let actorType: string | undefined;
|
||||
let actor: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
|
||||
const user = data.user;
|
||||
if (user) {
|
||||
actorType = "user";
|
||||
actor = user.username;
|
||||
actorId = user.userId;
|
||||
}
|
||||
const apiKey = data.apiKey;
|
||||
if (apiKey) {
|
||||
actorType = "apiKey";
|
||||
actor = apiKey.name || apiKey.apiKeyId;
|
||||
actorId = apiKey.apiKeyId;
|
||||
}
|
||||
|
||||
// if (!actorType || !actor || !actorId) {
|
||||
// logger.warn("logRequestAudit: Incomplete actor information");
|
||||
// return;
|
||||
// }
|
||||
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
let metadata = null;
|
||||
if (metadata) {
|
||||
metadata = JSON.stringify(metadata);
|
||||
}
|
||||
|
||||
const clientIp = body.requestIp
|
||||
? (() => {
|
||||
if (
|
||||
body.requestIp.startsWith("[") &&
|
||||
body.requestIp.includes("]")
|
||||
) {
|
||||
// if brackets are found, extract the IPv6 address from between the brackets
|
||||
const ipv6Match = body.requestIp.match(/\[(.*?)\]/);
|
||||
if (ipv6Match) {
|
||||
return ipv6Match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// ivp4
|
||||
// split at last colon
|
||||
const lastColonIndex = body.requestIp.lastIndexOf(":");
|
||||
if (lastColonIndex !== -1) {
|
||||
return body.requestIp.substring(0, lastColonIndex);
|
||||
}
|
||||
return body.requestIp;
|
||||
})()
|
||||
: undefined;
|
||||
|
||||
await db.insert(requestAuditLog).values({
|
||||
timestamp,
|
||||
orgId: data.orgId,
|
||||
actorType,
|
||||
actor,
|
||||
actorId,
|
||||
metadata,
|
||||
action: data.action,
|
||||
resourceId: data.resourceId,
|
||||
reason: data.reason,
|
||||
location: data.location,
|
||||
// userAgent: data.userAgent, // TODO: add this
|
||||
// headers: data.body.headers,
|
||||
// query: data.body.query,
|
||||
originalRequestURL: body.originalRequestURL,
|
||||
scheme: body.scheme,
|
||||
host: body.host,
|
||||
path: body.path,
|
||||
method: body.method,
|
||||
ip: clientIp,
|
||||
tls: body.tls
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import {
|
||||
createResourceSession,
|
||||
serializeResourceSessionCookie,
|
||||
validateResourceSessionToken
|
||||
} from "@server/auth/sessions/resource";
|
||||
import { validateResourceSessionToken } from "@server/auth/sessions/resource";
|
||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||
import {
|
||||
getResourceByDomain,
|
||||
@@ -18,7 +13,6 @@ import {
|
||||
LoginPage,
|
||||
Org,
|
||||
Resource,
|
||||
ResourceAccessToken,
|
||||
ResourceHeaderAuth,
|
||||
ResourcePassword,
|
||||
ResourcePincode,
|
||||
@@ -32,7 +26,6 @@ import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import NodeCache from "node-cache";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { getCountryCodeForIp } from "@server/lib/geoip";
|
||||
@@ -43,11 +36,8 @@ import {
|
||||
checkOrgAccessPolicy,
|
||||
enforceResourceSessionLength
|
||||
} from "#dynamic/lib/checkOrgAccessPolicy";
|
||||
|
||||
// We'll see if this speeds anything up
|
||||
const cache = new NodeCache({
|
||||
stdTTL: 5 // seconds
|
||||
});
|
||||
import { logRequestAudit } from "./logRequestAudit";
|
||||
import cache from "@server/lib/cache";
|
||||
|
||||
const verifyResourceSessionSchema = z.object({
|
||||
sessions: z.record(z.string()).optional(),
|
||||
@@ -133,6 +123,10 @@ export async function verifyResourceSession(
|
||||
|
||||
logger.debug("Client IP:", { clientIp });
|
||||
|
||||
const ipCC = clientIp
|
||||
? await getCountryCodeFromIp(clientIp)
|
||||
: undefined;
|
||||
|
||||
let cleanHost = host;
|
||||
// if the host ends with :port, strip it
|
||||
if (cleanHost.match(/:[0-9]{1,5}$/)) {
|
||||
@@ -156,17 +150,43 @@ export async function verifyResourceSession(
|
||||
|
||||
if (!result) {
|
||||
logger.debug(`Resource not found ${cleanHost}`);
|
||||
|
||||
// TODO: we cant log this for now because we dont know the org
|
||||
// eventually it would be cool to show this for the server admin
|
||||
|
||||
// logRequestAudit(
|
||||
// {
|
||||
// action: false,
|
||||
// reason: 201, //resource not found
|
||||
// location: ipCC
|
||||
// },
|
||||
// parsedBody.data
|
||||
// );
|
||||
|
||||
return notAllowed(res);
|
||||
}
|
||||
|
||||
resourceData = result;
|
||||
cache.set(resourceCacheKey, resourceData);
|
||||
cache.set(resourceCacheKey, resourceData, 5);
|
||||
}
|
||||
|
||||
const { resource, pincode, password, headerAuth } = resourceData;
|
||||
|
||||
if (!resource) {
|
||||
logger.debug(`Resource not found ${cleanHost}`);
|
||||
|
||||
// TODO: we cant log this for now because we dont know the org
|
||||
// eventually it would be cool to show this for the server admin
|
||||
|
||||
// logRequestAudit(
|
||||
// {
|
||||
// action: false,
|
||||
// reason: 201, //resource not found
|
||||
// location: ipCC
|
||||
// },
|
||||
// parsedBody.data
|
||||
// );
|
||||
|
||||
return notAllowed(res);
|
||||
}
|
||||
|
||||
@@ -174,6 +194,18 @@ export async function verifyResourceSession(
|
||||
|
||||
if (blockAccess) {
|
||||
logger.debug("Resource blocked", host);
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: false,
|
||||
reason: 202, //resource blocked
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return notAllowed(res);
|
||||
}
|
||||
|
||||
@@ -182,14 +214,40 @@ export async function verifyResourceSession(
|
||||
const action = await checkRules(
|
||||
resource.resourceId,
|
||||
clientIp,
|
||||
path
|
||||
path,
|
||||
ipCC
|
||||
);
|
||||
|
||||
if (action == "ACCEPT") {
|
||||
logger.debug("Resource allowed by rule");
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 100, // allowed by rule
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
} else if (action == "DROP") {
|
||||
logger.debug("Resource denied by rule");
|
||||
|
||||
// TODO: add rules type
|
||||
logRequestAudit(
|
||||
{
|
||||
action: false,
|
||||
reason: 203, // dropped by rules
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return notAllowed(res);
|
||||
} else if (action == "PASS") {
|
||||
logger.debug(
|
||||
@@ -210,6 +268,18 @@ export async function verifyResourceSession(
|
||||
!headerAuth
|
||||
) {
|
||||
logger.debug("Resource allowed because no auth");
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 101, // allowed no auth
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
}
|
||||
|
||||
@@ -261,6 +331,21 @@ export async function verifyResourceSession(
|
||||
}
|
||||
|
||||
if (valid && tokenItem) {
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 102, // valid access token
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC,
|
||||
apiKey: {
|
||||
name: tokenItem.title,
|
||||
apiKeyId: tokenItem.accessTokenId
|
||||
}
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
}
|
||||
}
|
||||
@@ -297,6 +382,21 @@ export async function verifyResourceSession(
|
||||
}
|
||||
|
||||
if (valid && tokenItem) {
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 102, // valid access token
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC,
|
||||
apiKey: {
|
||||
name: tokenItem.title,
|
||||
apiKeyId: tokenItem.accessTokenId
|
||||
}
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
}
|
||||
}
|
||||
@@ -308,6 +408,18 @@ export async function verifyResourceSession(
|
||||
logger.debug(
|
||||
"Resource allowed because header auth is valid (cached)"
|
||||
);
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 103, // valid header auth
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
} else if (
|
||||
await verifyPassword(
|
||||
@@ -315,8 +427,20 @@ export async function verifyResourceSession(
|
||||
headerAuth.headerAuthHash
|
||||
)
|
||||
) {
|
||||
cache.set(clientHeaderAuthKey, clientHeaderAuth);
|
||||
cache.set(clientHeaderAuthKey, clientHeaderAuth, 5);
|
||||
logger.debug("Resource allowed because header auth is valid");
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 103, // valid header auth
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
}
|
||||
|
||||
@@ -327,6 +451,17 @@ export async function verifyResourceSession(
|
||||
!password &&
|
||||
!resource.emailWhitelistEnabled
|
||||
) {
|
||||
logRequestAudit(
|
||||
{
|
||||
action: false,
|
||||
reason: 299, // no more auth methods
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return notAllowed(res);
|
||||
}
|
||||
} else if (headerAuth) {
|
||||
@@ -337,6 +472,17 @@ export async function verifyResourceSession(
|
||||
!password &&
|
||||
!resource.emailWhitelistEnabled
|
||||
) {
|
||||
logRequestAudit(
|
||||
{
|
||||
action: false,
|
||||
reason: 299, // no more auth methods
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return notAllowed(res);
|
||||
}
|
||||
}
|
||||
@@ -349,6 +495,18 @@ export async function verifyResourceSession(
|
||||
}. IP: ${clientIp}.`
|
||||
);
|
||||
}
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: false,
|
||||
reason: 204, // no sessions
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return notAllowed(res);
|
||||
}
|
||||
|
||||
@@ -368,7 +526,7 @@ export async function verifyResourceSession(
|
||||
);
|
||||
|
||||
resourceSession = result?.resourceSession;
|
||||
cache.set(sessionCacheKey, resourceSession);
|
||||
cache.set(sessionCacheKey, resourceSession, 5);
|
||||
}
|
||||
|
||||
if (resourceSession?.isRequestToken) {
|
||||
@@ -382,6 +540,18 @@ export async function verifyResourceSession(
|
||||
}. IP: ${clientIp}.`
|
||||
);
|
||||
}
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: false,
|
||||
reason: 205, // temporary request token
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return notAllowed(res);
|
||||
}
|
||||
|
||||
@@ -404,6 +574,18 @@ export async function verifyResourceSession(
|
||||
logger.debug(
|
||||
"Resource allowed because pincode session is valid"
|
||||
);
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 104, // valid pincode
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
}
|
||||
|
||||
@@ -411,6 +593,18 @@ export async function verifyResourceSession(
|
||||
logger.debug(
|
||||
"Resource allowed because password session is valid"
|
||||
);
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 105, // valid password
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
}
|
||||
|
||||
@@ -421,6 +615,18 @@ export async function verifyResourceSession(
|
||||
logger.debug(
|
||||
"Resource allowed because whitelist session is valid"
|
||||
);
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 106, // valid email
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
}
|
||||
|
||||
@@ -428,6 +634,22 @@ export async function verifyResourceSession(
|
||||
logger.debug(
|
||||
"Resource allowed because access token session is valid"
|
||||
);
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 102, // valid access token
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC,
|
||||
apiKey: {
|
||||
name: resourceSession.accessTokenTitle,
|
||||
apiKeyId: resourceSession.accessTokenId
|
||||
}
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
}
|
||||
|
||||
@@ -446,7 +668,7 @@ export async function verifyResourceSession(
|
||||
resourceData.org
|
||||
);
|
||||
|
||||
cache.set(userAccessCacheKey, allowedUserData);
|
||||
cache.set(userAccessCacheKey, allowedUserData, 5);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -456,6 +678,22 @@ export async function verifyResourceSession(
|
||||
logger.debug(
|
||||
"Resource allowed because user session is valid"
|
||||
);
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: true,
|
||||
reason: 107, // valid sso
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC,
|
||||
user: {
|
||||
username: allowedUserData.username,
|
||||
userId: resourceSession.userId
|
||||
}
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res, allowedUserData);
|
||||
}
|
||||
}
|
||||
@@ -474,6 +712,17 @@ export async function verifyResourceSession(
|
||||
|
||||
logger.debug(`Redirecting to login at ${redirectPath}`);
|
||||
|
||||
logRequestAudit(
|
||||
{
|
||||
action: false,
|
||||
reason: 299, // no more auth methods
|
||||
resourceId: resource.resourceId,
|
||||
orgId: resource.orgId,
|
||||
location: ipCC
|
||||
},
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return notAllowed(res, redirectPath, resource.orgId);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -657,7 +906,8 @@ async function isUserAllowedToAccessResource(
|
||||
async function checkRules(
|
||||
resourceId: number,
|
||||
clientIp: string | undefined,
|
||||
path: string | undefined
|
||||
path: string | undefined,
|
||||
ipCC?: string
|
||||
): Promise<"ACCEPT" | "DROP" | "PASS" | undefined> {
|
||||
const ruleCacheKey = `rules:${resourceId}`;
|
||||
|
||||
@@ -665,7 +915,7 @@ async function checkRules(
|
||||
|
||||
if (!rules) {
|
||||
rules = await getResourceRules(resourceId);
|
||||
cache.set(ruleCacheKey, rules);
|
||||
cache.set(ruleCacheKey, rules, 5);
|
||||
}
|
||||
|
||||
if (rules.length === 0) {
|
||||
@@ -697,7 +947,7 @@ async function checkRules(
|
||||
return rule.action as any;
|
||||
} else if (
|
||||
clientIp &&
|
||||
rule.match == "GEOIP" &&
|
||||
rule.match == "COUNTRY" &&
|
||||
(await isIpInGeoIP(clientIp, rule.value))
|
||||
) {
|
||||
return rule.action as any;
|
||||
@@ -826,11 +1076,20 @@ export function isPathAllowed(pattern: string, path: string): boolean {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function isIpInGeoIP(ip: string, countryCode: string): Promise<boolean> {
|
||||
if (countryCode == "ALL") {
|
||||
async function isIpInGeoIP(
|
||||
ipCountryCode: string,
|
||||
checkCountryCode: string
|
||||
): Promise<boolean> {
|
||||
if (checkCountryCode == "ALL") {
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.debug(`IP ${ipCountryCode} is in country: ${checkCountryCode}`);
|
||||
|
||||
return ipCountryCode?.toUpperCase() === checkCountryCode.toUpperCase();
|
||||
}
|
||||
|
||||
async function getCountryCodeFromIp(ip: string): Promise<string | undefined> {
|
||||
const geoIpCacheKey = `geoip:${ip}`;
|
||||
|
||||
let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey);
|
||||
@@ -841,9 +1100,7 @@ async function isIpInGeoIP(ip: string, countryCode: string): Promise<boolean> {
|
||||
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
||||
}
|
||||
|
||||
logger.debug(`IP ${ip} is in country: ${cachedCountryCode}`);
|
||||
|
||||
return cachedCountryCode?.toUpperCase() === countryCode.toUpperCase();
|
||||
return cachedCountryCode;
|
||||
}
|
||||
|
||||
function extractBasicAuth(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db } from "@server/db";
|
||||
import { db, olms } from "@server/db";
|
||||
import {
|
||||
clients,
|
||||
orgs,
|
||||
@@ -16,6 +16,67 @@ import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import NodeCache from "node-cache";
|
||||
import semver from "semver";
|
||||
|
||||
const olmVersionCache = new NodeCache({ stdTTL: 3600 });
|
||||
|
||||
async function getLatestOlmVersion(): Promise<string | null> {
|
||||
try {
|
||||
const cachedVersion = olmVersionCache.get<string>("latestOlmVersion");
|
||||
if (cachedVersion) {
|
||||
return cachedVersion;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1500);
|
||||
|
||||
const response = await fetch(
|
||||
"https://api.github.com/repos/fosrl/olm/tags",
|
||||
{
|
||||
signal: controller.signal
|
||||
}
|
||||
);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn(
|
||||
`Failed to fetch latest Olm version from GitHub: ${response.status} ${response.statusText}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const tags = await response.json();
|
||||
if (!Array.isArray(tags) || tags.length === 0) {
|
||||
logger.warn("No tags found for Olm repository");
|
||||
return null;
|
||||
}
|
||||
|
||||
const latestVersion = tags[0].name;
|
||||
|
||||
olmVersionCache.set("latestOlmVersion", latestVersion);
|
||||
|
||||
return latestVersion;
|
||||
} catch (error: any) {
|
||||
if (error.name === "AbortError") {
|
||||
logger.warn(
|
||||
"Request to fetch latest Olm version timed out (1.5s)"
|
||||
);
|
||||
} else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
|
||||
logger.warn(
|
||||
"Connection timeout while fetching latest Olm version"
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
"Error fetching latest Olm version:",
|
||||
error.message || error
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const listClientsParamsSchema = z
|
||||
.object({
|
||||
@@ -50,10 +111,12 @@ function queryClients(orgId: string, accessibleClientIds: number[]) {
|
||||
megabytesOut: clients.megabytesOut,
|
||||
orgName: orgs.name,
|
||||
type: clients.type,
|
||||
online: clients.online
|
||||
online: clients.online,
|
||||
olmVersion: olms.version
|
||||
})
|
||||
.from(clients)
|
||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||
.where(
|
||||
and(
|
||||
inArray(clients.clientId, accessibleClientIds),
|
||||
@@ -77,12 +140,20 @@ async function getSiteAssociations(clientIds: number[]) {
|
||||
.where(inArray(clientSites.clientId, clientIds));
|
||||
}
|
||||
|
||||
type OlmWithUpdateAvailable = Awaited<ReturnType<typeof queryClients>>[0] & {
|
||||
olmUpdateAvailable?: boolean;
|
||||
};
|
||||
|
||||
|
||||
export type ListClientsResponse = {
|
||||
clients: Array<Awaited<ReturnType<typeof queryClients>>[0] & { sites: Array<{
|
||||
siteId: number;
|
||||
siteName: string | null;
|
||||
siteNiceId: string | null;
|
||||
}> }>;
|
||||
clients: Array<Awaited<ReturnType<typeof queryClients>>[0] & {
|
||||
sites: Array<{
|
||||
siteId: number;
|
||||
siteName: string | null;
|
||||
siteNiceId: string | null;
|
||||
}>
|
||||
olmUpdateAvailable?: boolean;
|
||||
}>;
|
||||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
@@ -206,6 +277,43 @@ export async function listClients(
|
||||
sites: sitesByClient[client.clientId] || []
|
||||
}));
|
||||
|
||||
const latestOlVersionPromise = getLatestOlmVersion();
|
||||
|
||||
const olmsWithUpdates: OlmWithUpdateAvailable[] = clientsWithSites.map(
|
||||
(client) => {
|
||||
const OlmWithUpdate: OlmWithUpdateAvailable = { ...client };
|
||||
// Initially set to false, will be updated if version check succeeds
|
||||
OlmWithUpdate.olmUpdateAvailable = false;
|
||||
return OlmWithUpdate;
|
||||
}
|
||||
);
|
||||
|
||||
// Try to get the latest version, but don't block if it fails
|
||||
try {
|
||||
const latestOlVersion = await latestOlVersionPromise;
|
||||
|
||||
if (latestOlVersion) {
|
||||
olmsWithUpdates.forEach((client) => {
|
||||
try {
|
||||
client.olmUpdateAvailable = semver.lt(
|
||||
client.olmVersion ? client.olmVersion : "",
|
||||
latestOlVersion
|
||||
);
|
||||
} catch (error) {
|
||||
client.olmUpdateAvailable = false;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Log the error but don't let it block the response
|
||||
logger.warn(
|
||||
"Failed to check for OLM updates, continuing without update info:",
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return response<ListClientsResponse>(res, {
|
||||
data: {
|
||||
clients: clientsWithSites,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, Domain, domains, OrgDomains, orgDomains } from "@server/db";
|
||||
import { db, Domain, domains, OrgDomains, orgDomains, dnsRecords } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -24,16 +24,21 @@ const paramsSchema = z
|
||||
const bodySchema = z
|
||||
.object({
|
||||
type: z.enum(["ns", "cname", "wildcard"]),
|
||||
baseDomain: subdomainSchema
|
||||
baseDomain: subdomainSchema,
|
||||
certResolver: z.string().optional().nullable(),
|
||||
preferWildcardCert: z.boolean().optional().nullable() // optional, only for wildcard
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
export type CreateDomainResponse = {
|
||||
domainId: string;
|
||||
nsRecords?: string[];
|
||||
cnameRecords?: { baseDomain: string; value: string }[];
|
||||
aRecords?: { baseDomain: string; value: string }[];
|
||||
txtRecords?: { baseDomain: string; value: string }[];
|
||||
certResolver?: string | null;
|
||||
preferWildcardCert?: boolean | null;
|
||||
};
|
||||
|
||||
// Helper to check if a domain is a subdomain or equal to another domain
|
||||
@@ -71,7 +76,7 @@ export async function createOrgDomain(
|
||||
}
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
const { type, baseDomain } = parsedBody.data;
|
||||
const { type, baseDomain, certResolver, preferWildcardCert } = parsedBody.data;
|
||||
|
||||
if (build == "oss") {
|
||||
if (type !== "wildcard") {
|
||||
@@ -254,7 +259,9 @@ export async function createOrgDomain(
|
||||
domainId,
|
||||
baseDomain,
|
||||
type,
|
||||
verified: type === "wildcard" ? true : false
|
||||
verified: type === "wildcard" ? true : false,
|
||||
certResolver: certResolver || null,
|
||||
preferWildcardCert: preferWildcardCert || false
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -269,9 +276,24 @@ export async function createOrgDomain(
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Prepare DNS records to insert
|
||||
const recordsToInsert = [];
|
||||
|
||||
// TODO: This needs to be cross region and not hardcoded
|
||||
if (type === "ns") {
|
||||
nsRecords = config.getRawConfig().dns.nameservers as string[];
|
||||
|
||||
// Save NS records to database
|
||||
for (const nsValue of nsRecords) {
|
||||
recordsToInsert.push({
|
||||
id: generateId(15),
|
||||
domainId,
|
||||
recordType: "NS",
|
||||
baseDomain: baseDomain,
|
||||
value: nsValue,
|
||||
verified: false
|
||||
});
|
||||
}
|
||||
} else if (type === "cname") {
|
||||
cnameRecords = [
|
||||
{
|
||||
@@ -283,6 +305,18 @@ export async function createOrgDomain(
|
||||
baseDomain: `_acme-challenge.${baseDomain}`
|
||||
}
|
||||
];
|
||||
|
||||
// Save CNAME records to database
|
||||
for (const cnameRecord of cnameRecords) {
|
||||
recordsToInsert.push({
|
||||
id: generateId(15),
|
||||
domainId,
|
||||
recordType: "CNAME",
|
||||
baseDomain: cnameRecord.baseDomain,
|
||||
value: cnameRecord.value,
|
||||
verified: false
|
||||
});
|
||||
}
|
||||
} else if (type === "wildcard") {
|
||||
aRecords = [
|
||||
{
|
||||
@@ -294,6 +328,23 @@ export async function createOrgDomain(
|
||||
baseDomain: `${baseDomain}`
|
||||
}
|
||||
];
|
||||
|
||||
// Save A records to database
|
||||
for (const aRecord of aRecords) {
|
||||
recordsToInsert.push({
|
||||
id: generateId(15),
|
||||
domainId,
|
||||
recordType: "A",
|
||||
baseDomain: aRecord.baseDomain,
|
||||
value: aRecord.value,
|
||||
verified: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Insert all DNS records in batch
|
||||
if (recordsToInsert.length > 0) {
|
||||
await trx.insert(dnsRecords).values(recordsToInsert);
|
||||
}
|
||||
|
||||
numOrgDomains = await trx
|
||||
@@ -325,7 +376,9 @@ export async function createOrgDomain(
|
||||
cnameRecords,
|
||||
txtRecords,
|
||||
nsRecords,
|
||||
aRecords
|
||||
aRecords,
|
||||
certResolver: returned.certResolver,
|
||||
preferWildcardCert: returned.preferWildcardCert
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
|
||||
97
server/routers/domain/getDNSRecords.ts
Normal file
97
server/routers/domain/getDNSRecords.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, dnsRecords } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { getServerIp } from "@server/lib/serverIpService"; // your in-memory IP module
|
||||
|
||||
const getDNSRecordsSchema = z
|
||||
.object({
|
||||
domainId: z.string(),
|
||||
orgId: z.string()
|
||||
})
|
||||
.strict();
|
||||
|
||||
async function query(domainId: string) {
|
||||
const records = await db
|
||||
.select()
|
||||
.from(dnsRecords)
|
||||
.where(eq(dnsRecords.domainId, domainId));
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
export type GetDNSRecordsResponse = Awaited<ReturnType<typeof query>>;
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/domain/{domainId}/dns-records",
|
||||
description: "Get all DNS records for a domain by domainId.",
|
||||
tags: [OpenAPITags.Domain],
|
||||
request: {
|
||||
params: z.object({
|
||||
domainId: z.string(),
|
||||
orgId: z.string()
|
||||
})
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getDNSRecords(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = getDNSRecordsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { domainId } = parsedParams.data;
|
||||
|
||||
const records = await query(domainId);
|
||||
|
||||
if (!records || records.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"No DNS records found for this domain"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const serverIp = getServerIp();
|
||||
|
||||
// Override value for type A or wildcard records
|
||||
const updatedRecords = records.map(record => {
|
||||
if ((record.recordType === "A" || record.baseDomain === "*") && serverIp) {
|
||||
return { ...record, value: serverIp };
|
||||
}
|
||||
return record;
|
||||
});
|
||||
|
||||
return response<GetDNSRecordsResponse>(res, {
|
||||
data: updatedRecords,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "DNS records retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
86
server/routers/domain/getDomain.ts
Normal file
86
server/routers/domain/getDomain.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, domains } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { domain } from "zod/v4/core/regexes";
|
||||
|
||||
const getDomainSchema = z
|
||||
.object({
|
||||
domainId: z
|
||||
.string()
|
||||
.optional(),
|
||||
orgId: z.string().optional()
|
||||
})
|
||||
.strict();
|
||||
|
||||
async function query(domainId?: string, orgId?: string) {
|
||||
if (domainId) {
|
||||
const [res] = await db
|
||||
.select()
|
||||
.from(domains)
|
||||
.where(eq(domains.domainId, domainId))
|
||||
.limit(1);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export type GetDomainResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/org/{orgId}/domain/{domainId}",
|
||||
description: "Get a domain by domainId.",
|
||||
tags: [OpenAPITags.Domain],
|
||||
request: {
|
||||
params: z.object({
|
||||
domainId: z.string(),
|
||||
orgId: z.string()
|
||||
})
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getDomain(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = getDomainSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, domainId } = parsedParams.data;
|
||||
|
||||
const domain = await query(domainId, orgId);
|
||||
|
||||
if (!domain) {
|
||||
return next(createHttpError(HttpCode.NOT_FOUND, "Domain not found"));
|
||||
}
|
||||
|
||||
return response<GetDomainResponse>(res, {
|
||||
data: domain,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Domain retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
export * from "./listDomains";
|
||||
export * from "./createOrgDomain";
|
||||
export * from "./deleteOrgDomain";
|
||||
export * from "./restartOrgDomain";
|
||||
export * from "./restartOrgDomain";
|
||||
export * from "./getDomain";
|
||||
export * from "./getDNSRecords";
|
||||
export * from "./updateDomain";
|
||||
@@ -42,7 +42,9 @@ async function queryDomains(orgId: string, limit: number, offset: number) {
|
||||
type: domains.type,
|
||||
failed: domains.failed,
|
||||
tries: domains.tries,
|
||||
configManaged: domains.configManaged
|
||||
configManaged: domains.configManaged,
|
||||
certResolver: domains.certResolver,
|
||||
preferWildcardCert: domains.preferWildcardCert
|
||||
})
|
||||
.from(orgDomains)
|
||||
.where(eq(orgDomains.orgId, orgId))
|
||||
|
||||
161
server/routers/domain/updateDomain.ts
Normal file
161
server/routers/domain/updateDomain.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, domains, orgDomains } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
orgId: z.string(),
|
||||
domainId: z.string()
|
||||
})
|
||||
.strict();
|
||||
|
||||
const bodySchema = z
|
||||
.object({
|
||||
certResolver: z.string().optional().nullable(),
|
||||
preferWildcardCert: z.boolean().optional().nullable()
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type UpdateDomainResponse = {
|
||||
domainId: string;
|
||||
certResolver: string | null;
|
||||
preferWildcardCert: boolean | null;
|
||||
};
|
||||
|
||||
|
||||
registry.registerPath({
|
||||
method: "patch",
|
||||
path: "/org/{orgId}/domain/{domainId}",
|
||||
description: "Update a domain by domainId.",
|
||||
tags: [OpenAPITags.Domain],
|
||||
request: {
|
||||
params: z.object({
|
||||
domainId: z.string(),
|
||||
orgId: z.string()
|
||||
})
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function updateOrgDomain(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, domainId } = parsedParams.data;
|
||||
const { certResolver, preferWildcardCert } = parsedBody.data;
|
||||
|
||||
const [orgDomain] = await db
|
||||
.select()
|
||||
.from(orgDomains)
|
||||
.where(
|
||||
and(
|
||||
eq(orgDomains.orgId, orgId),
|
||||
eq(orgDomains.domainId, domainId)
|
||||
)
|
||||
);
|
||||
|
||||
if (!orgDomain) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Domain not found or does not belong to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const [existingDomain] = await db
|
||||
.select()
|
||||
.from(domains)
|
||||
.where(eq(domains.domainId, domainId));
|
||||
|
||||
if (!existingDomain) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Domain not found")
|
||||
);
|
||||
}
|
||||
|
||||
if (existingDomain.type !== "wildcard") {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Domain settings can only be updated for wildcard domains"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const updateData: Partial<{
|
||||
certResolver: string | null;
|
||||
preferWildcardCert: boolean;
|
||||
}> = {};
|
||||
|
||||
if (certResolver !== undefined) {
|
||||
updateData.certResolver = certResolver;
|
||||
}
|
||||
|
||||
if (preferWildcardCert !== undefined && preferWildcardCert !== null) {
|
||||
updateData.preferWildcardCert = preferWildcardCert;
|
||||
}
|
||||
|
||||
const [updatedDomain] = await db
|
||||
.update(domains)
|
||||
.set(updateData)
|
||||
.where(eq(domains.domainId, domainId))
|
||||
.returning();
|
||||
|
||||
if (!updatedDomain) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to update domain"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response<UpdateDomainResponse>(res, {
|
||||
data: {
|
||||
domainId: updatedDomain.domainId,
|
||||
certResolver: updatedDomain.certResolver,
|
||||
preferWildcardCert: updatedDomain.preferWildcardCert
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Domain updated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import * as supporterKey from "./supporterKey";
|
||||
import * as accessToken from "./accessToken";
|
||||
import * as idp from "./idp";
|
||||
import * as apiKeys from "./apiKeys";
|
||||
import * as logs from "./auditLogs";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import {
|
||||
verifyAccessTokenAccess,
|
||||
@@ -44,6 +45,8 @@ import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||
import createHttpError from "http-errors";
|
||||
import { build } from "@server/build";
|
||||
import { createStore } from "#dynamic/lib/rateLimitStore";
|
||||
import { logActionAudit } from "#dynamic/middlewares";
|
||||
import { log } from "console";
|
||||
|
||||
// Root routes
|
||||
export const unauthenticated = Router();
|
||||
@@ -75,7 +78,8 @@ authenticated.post(
|
||||
"/org/:orgId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateOrg),
|
||||
org.updateOrg
|
||||
logActionAudit(ActionsEnum.updateOrg),
|
||||
org.updateOrg,
|
||||
);
|
||||
|
||||
if (build !== "saas") {
|
||||
@@ -84,7 +88,8 @@ if (build !== "saas") {
|
||||
verifyOrgAccess,
|
||||
verifyUserIsOrgOwner,
|
||||
verifyUserHasAction(ActionsEnum.deleteOrg),
|
||||
org.deleteOrg
|
||||
logActionAudit(ActionsEnum.deleteOrg),
|
||||
org.deleteOrg,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,6 +97,7 @@ authenticated.put(
|
||||
"/org/:orgId/site",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createSite),
|
||||
logActionAudit(ActionsEnum.createSite),
|
||||
site.createSite
|
||||
);
|
||||
authenticated.get(
|
||||
@@ -149,7 +155,8 @@ authenticated.put(
|
||||
verifyClientsEnabled,
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createClient),
|
||||
client.createClient
|
||||
logActionAudit(ActionsEnum.createClient),
|
||||
client.createClient,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -157,7 +164,8 @@ authenticated.delete(
|
||||
verifyClientsEnabled,
|
||||
verifyClientAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteClient),
|
||||
client.deleteClient
|
||||
logActionAudit(ActionsEnum.deleteClient),
|
||||
client.deleteClient,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -165,7 +173,8 @@ authenticated.post(
|
||||
verifyClientsEnabled,
|
||||
verifyClientAccess, // this will check if the user has access to the client
|
||||
verifyUserHasAction(ActionsEnum.updateClient), // this will check if the user has permission to update the client
|
||||
client.updateClient
|
||||
logActionAudit(ActionsEnum.updateClient),
|
||||
client.updateClient,
|
||||
);
|
||||
|
||||
// authenticated.get(
|
||||
@@ -178,15 +187,18 @@ authenticated.post(
|
||||
"/site/:siteId",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateSite),
|
||||
site.updateSite
|
||||
logActionAudit(ActionsEnum.updateSite),
|
||||
site.updateSite,
|
||||
);
|
||||
authenticated.delete(
|
||||
"/site/:siteId",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteSite),
|
||||
site.deleteSite
|
||||
logActionAudit(ActionsEnum.deleteSite),
|
||||
site.deleteSite,
|
||||
);
|
||||
|
||||
// TODO: BREAK OUT THESE ACTIONS SO THEY ARE NOT ALL "getSite"
|
||||
authenticated.get(
|
||||
"/site/:siteId/docker/status",
|
||||
verifySiteAccess,
|
||||
@@ -203,13 +215,13 @@ authenticated.post(
|
||||
"/site/:siteId/docker/check",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.getSite),
|
||||
site.checkDockerSocket
|
||||
site.checkDockerSocket,
|
||||
);
|
||||
authenticated.post(
|
||||
"/site/:siteId/docker/trigger",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.getSite),
|
||||
site.triggerFetchContainers
|
||||
site.triggerFetchContainers,
|
||||
);
|
||||
authenticated.get(
|
||||
"/site/:siteId/docker/containers",
|
||||
@@ -224,7 +236,8 @@ authenticated.put(
|
||||
verifyOrgAccess,
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.createSiteResource),
|
||||
siteResource.createSiteResource
|
||||
logActionAudit(ActionsEnum.createSiteResource),
|
||||
siteResource.createSiteResource,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -257,7 +270,8 @@ authenticated.post(
|
||||
verifySiteAccess,
|
||||
verifySiteResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateSiteResource),
|
||||
siteResource.updateSiteResource
|
||||
logActionAudit(ActionsEnum.updateSiteResource),
|
||||
siteResource.updateSiteResource,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -266,14 +280,16 @@ authenticated.delete(
|
||||
verifySiteAccess,
|
||||
verifySiteResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteSiteResource),
|
||||
siteResource.deleteSiteResource
|
||||
logActionAudit(ActionsEnum.deleteSiteResource),
|
||||
siteResource.deleteSiteResource,
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/resource",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createResource),
|
||||
resource.createResource
|
||||
logActionAudit(ActionsEnum.createResource),
|
||||
resource.createResource,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -302,6 +318,27 @@ authenticated.get(
|
||||
domain.listDomains
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/domain/:domainId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.getDomain),
|
||||
domain.getDomain
|
||||
);
|
||||
|
||||
authenticated.patch(
|
||||
"/org/:orgId/domain/:domainId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateOrgDomain),
|
||||
domain.updateOrgDomain
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/domain/:domainId/dns-records",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.getDNSRecords),
|
||||
domain.getDNSRecords
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/invitations",
|
||||
verifyOrgAccess,
|
||||
@@ -313,15 +350,18 @@ authenticated.delete(
|
||||
"/org/:orgId/invitations/:inviteId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.removeInvitation),
|
||||
user.removeInvitation
|
||||
logActionAudit(ActionsEnum.removeInvitation),
|
||||
user.removeInvitation,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/create-invite",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.inviteUser),
|
||||
user.inviteUser
|
||||
logActionAudit(ActionsEnum.inviteUser),
|
||||
user.inviteUser,
|
||||
); // maybe make this /invite/create instead
|
||||
|
||||
unauthenticated.post("/invite/accept", user.acceptInvite); // this is supposed to be unauthenticated
|
||||
|
||||
authenticated.get(
|
||||
@@ -354,20 +394,23 @@ authenticated.post(
|
||||
"/resource/:resourceId",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateResource),
|
||||
resource.updateResource
|
||||
logActionAudit(ActionsEnum.updateResource),
|
||||
resource.updateResource,
|
||||
);
|
||||
authenticated.delete(
|
||||
"/resource/:resourceId",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteResource),
|
||||
resource.deleteResource
|
||||
logActionAudit(ActionsEnum.deleteResource),
|
||||
resource.deleteResource,
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource/:resourceId/target",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.createTarget),
|
||||
target.createTarget
|
||||
logActionAudit(ActionsEnum.createTarget),
|
||||
target.createTarget,
|
||||
);
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/targets",
|
||||
@@ -380,7 +423,8 @@ authenticated.put(
|
||||
"/resource/:resourceId/rule",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.createResourceRule),
|
||||
resource.createResourceRule
|
||||
logActionAudit(ActionsEnum.createResourceRule),
|
||||
resource.createResourceRule,
|
||||
);
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/rules",
|
||||
@@ -392,13 +436,15 @@ authenticated.post(
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateResourceRule),
|
||||
resource.updateResourceRule
|
||||
logActionAudit(ActionsEnum.updateResourceRule),
|
||||
resource.updateResourceRule,
|
||||
);
|
||||
authenticated.delete(
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteResourceRule),
|
||||
resource.deleteResourceRule
|
||||
logActionAudit(ActionsEnum.deleteResourceRule),
|
||||
resource.deleteResourceRule,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -411,20 +457,23 @@ authenticated.post(
|
||||
"/target/:targetId",
|
||||
verifyTargetAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateTarget),
|
||||
target.updateTarget
|
||||
logActionAudit(ActionsEnum.updateTarget),
|
||||
target.updateTarget,
|
||||
);
|
||||
authenticated.delete(
|
||||
"/target/:targetId",
|
||||
verifyTargetAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteTarget),
|
||||
target.deleteTarget
|
||||
logActionAudit(ActionsEnum.deleteTarget),
|
||||
target.deleteTarget,
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/role",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createRole),
|
||||
role.createRole
|
||||
logActionAudit(ActionsEnum.createRole),
|
||||
role.createRole,
|
||||
);
|
||||
authenticated.get(
|
||||
"/org/:orgId/roles",
|
||||
@@ -449,14 +498,16 @@ authenticated.delete(
|
||||
"/role/:roleId",
|
||||
verifyRoleAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteRole),
|
||||
role.deleteRole
|
||||
logActionAudit(ActionsEnum.deleteRole),
|
||||
role.deleteRole,
|
||||
);
|
||||
authenticated.post(
|
||||
"/role/:roleId/add/:userId",
|
||||
verifyRoleAccess,
|
||||
verifyUserAccess,
|
||||
verifyUserHasAction(ActionsEnum.addUserRole),
|
||||
user.addUserRole
|
||||
logActionAudit(ActionsEnum.addUserRole),
|
||||
user.addUserRole,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -464,7 +515,8 @@ authenticated.post(
|
||||
verifyResourceAccess,
|
||||
verifyRoleAccess,
|
||||
verifyUserHasAction(ActionsEnum.setResourceRoles),
|
||||
resource.setResourceRoles
|
||||
logActionAudit(ActionsEnum.setResourceRoles),
|
||||
resource.setResourceRoles,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -472,35 +524,40 @@ authenticated.post(
|
||||
verifyResourceAccess,
|
||||
verifySetResourceUsers,
|
||||
verifyUserHasAction(ActionsEnum.setResourceUsers),
|
||||
resource.setResourceUsers
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
resource.setResourceUsers,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/password`,
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.setResourcePassword),
|
||||
resource.setResourcePassword
|
||||
logActionAudit(ActionsEnum.setResourcePassword),
|
||||
resource.setResourcePassword,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/pincode`,
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.setResourcePincode),
|
||||
resource.setResourcePincode
|
||||
logActionAudit(ActionsEnum.setResourcePincode),
|
||||
resource.setResourcePincode,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/header-auth`,
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.setResourceHeaderAuth),
|
||||
resource.setResourceHeaderAuth
|
||||
logActionAudit(ActionsEnum.setResourceHeaderAuth),
|
||||
resource.setResourceHeaderAuth,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/whitelist`,
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.setResourceWhitelist),
|
||||
resource.setResourceWhitelist
|
||||
logActionAudit(ActionsEnum.setResourceWhitelist),
|
||||
resource.setResourceWhitelist,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -514,14 +571,16 @@ authenticated.post(
|
||||
`/resource/:resourceId/access-token`,
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.generateAccessToken),
|
||||
accessToken.generateAccessToken
|
||||
logActionAudit(ActionsEnum.generateAccessToken),
|
||||
accessToken.generateAccessToken,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
`/access-token/:accessTokenId`,
|
||||
verifyAccessTokenAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteAcessToken),
|
||||
accessToken.deleteAccessToken
|
||||
logActionAudit(ActionsEnum.deleteAcessToken),
|
||||
accessToken.deleteAccessToken,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -594,7 +653,8 @@ authenticated.put(
|
||||
"/org/:orgId/user",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createOrgUser),
|
||||
user.createOrgUser
|
||||
logActionAudit(ActionsEnum.createOrgUser),
|
||||
user.createOrgUser,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -602,7 +662,8 @@ authenticated.post(
|
||||
verifyOrgAccess,
|
||||
verifyUserAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateOrgUser),
|
||||
user.updateOrgUser
|
||||
logActionAudit(ActionsEnum.updateOrgUser),
|
||||
user.updateOrgUser,
|
||||
);
|
||||
|
||||
authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser);
|
||||
@@ -625,7 +686,8 @@ authenticated.delete(
|
||||
verifyOrgAccess,
|
||||
verifyUserAccess,
|
||||
verifyUserHasAction(ActionsEnum.removeUser),
|
||||
user.removeUserOrg
|
||||
logActionAudit(ActionsEnum.removeUser),
|
||||
user.removeUserOrg,
|
||||
);
|
||||
|
||||
// authenticated.put(
|
||||
@@ -755,7 +817,8 @@ authenticated.post(
|
||||
verifyOrgAccess,
|
||||
verifyApiKeyAccess,
|
||||
verifyUserHasAction(ActionsEnum.setApiKeyActions),
|
||||
apiKeys.setApiKeyActions
|
||||
logActionAudit(ActionsEnum.setApiKeyActions),
|
||||
apiKeys.setApiKeyActions,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -770,7 +833,8 @@ authenticated.put(
|
||||
`/org/:orgId/api-key`,
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createApiKey),
|
||||
apiKeys.createOrgApiKey
|
||||
logActionAudit(ActionsEnum.createApiKey),
|
||||
apiKeys.createOrgApiKey,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -778,7 +842,8 @@ authenticated.delete(
|
||||
verifyOrgAccess,
|
||||
verifyApiKeyAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteApiKey),
|
||||
apiKeys.deleteOrgApiKey
|
||||
logActionAudit(ActionsEnum.deleteApiKey),
|
||||
apiKeys.deleteOrgApiKey,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -793,7 +858,8 @@ authenticated.put(
|
||||
`/org/:orgId/domain`,
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.createOrgDomain),
|
||||
domain.createOrgDomain
|
||||
logActionAudit(ActionsEnum.createOrgDomain),
|
||||
domain.createOrgDomain,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -801,7 +867,8 @@ authenticated.post(
|
||||
verifyOrgAccess,
|
||||
verifyDomainAccess,
|
||||
verifyUserHasAction(ActionsEnum.restartOrgDomain),
|
||||
domain.restartOrgDomain
|
||||
logActionAudit(ActionsEnum.restartOrgDomain),
|
||||
domain.restartOrgDomain,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -809,7 +876,23 @@ authenticated.delete(
|
||||
verifyOrgAccess,
|
||||
verifyDomainAccess,
|
||||
verifyUserHasAction(ActionsEnum.deleteOrgDomain),
|
||||
domain.deleteAccountDomain
|
||||
logActionAudit(ActionsEnum.deleteOrgDomain),
|
||||
domain.deleteAccountDomain,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/logs/request",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.viewLogs),
|
||||
logs.queryRequestAuditLogs
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/logs/request/export",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.exportLogs),
|
||||
logActionAudit(ActionsEnum.exportLogs),
|
||||
logs.exportRequestAuditLogs
|
||||
);
|
||||
|
||||
// Auth routes
|
||||
@@ -1132,4 +1215,4 @@ authRouter.delete(
|
||||
store: createStore()
|
||||
}),
|
||||
auth.deleteSecurityKey
|
||||
);
|
||||
);
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { Router } from "express";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import { build } from "@server/build";
|
||||
import { logActionAudit } from "#dynamic/middlewares";
|
||||
|
||||
export const unauthenticated = Router();
|
||||
|
||||
@@ -51,7 +51,8 @@ authenticated.put(
|
||||
"/org",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.createOrg),
|
||||
org.createOrg
|
||||
logActionAudit(ActionsEnum.createOrg),
|
||||
org.createOrg,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -72,21 +73,24 @@ authenticated.post(
|
||||
"/org/:orgId",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateOrg),
|
||||
org.updateOrg
|
||||
logActionAudit(ActionsEnum.updateOrg),
|
||||
org.updateOrg,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/org/:orgId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteOrg),
|
||||
org.deleteOrg
|
||||
logActionAudit(ActionsEnum.deleteOrg),
|
||||
org.deleteOrg,
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/site",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createSite),
|
||||
site.createSite
|
||||
logActionAudit(ActionsEnum.createSite),
|
||||
site.createSite,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -121,14 +125,16 @@ authenticated.post(
|
||||
"/site/:siteId",
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateSite),
|
||||
site.updateSite
|
||||
logActionAudit(ActionsEnum.updateSite),
|
||||
site.updateSite,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/site/:siteId",
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteSite),
|
||||
site.deleteSite
|
||||
logActionAudit(ActionsEnum.deleteSite),
|
||||
site.deleteSite,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -142,7 +148,8 @@ authenticated.put(
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createSiteResource),
|
||||
siteResource.createSiteResource
|
||||
logActionAudit(ActionsEnum.createSiteResource),
|
||||
siteResource.createSiteResource,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -175,7 +182,8 @@ authenticated.post(
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateSiteResource),
|
||||
siteResource.updateSiteResource
|
||||
logActionAudit(ActionsEnum.updateSiteResource),
|
||||
siteResource.updateSiteResource,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -184,21 +192,24 @@ authenticated.delete(
|
||||
verifyApiKeySiteAccess,
|
||||
verifyApiKeySiteResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteSiteResource),
|
||||
siteResource.deleteSiteResource
|
||||
logActionAudit(ActionsEnum.deleteSiteResource),
|
||||
siteResource.deleteSiteResource,
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/resource",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResource),
|
||||
resource.createResource
|
||||
logActionAudit(ActionsEnum.createResource),
|
||||
resource.createResource,
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/site/:siteId/resource",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResource),
|
||||
resource.createResource
|
||||
logActionAudit(ActionsEnum.createResource),
|
||||
resource.createResource,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -233,7 +244,8 @@ authenticated.post(
|
||||
"/org/:orgId/create-invite",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.inviteUser),
|
||||
user.inviteUser
|
||||
logActionAudit(ActionsEnum.inviteUser),
|
||||
user.inviteUser,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -261,21 +273,24 @@ authenticated.post(
|
||||
"/resource/:resourceId",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResource),
|
||||
resource.updateResource
|
||||
logActionAudit(ActionsEnum.updateResource),
|
||||
resource.updateResource,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/resource/:resourceId",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteResource),
|
||||
resource.deleteResource
|
||||
logActionAudit(ActionsEnum.deleteResource),
|
||||
resource.deleteResource,
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource/:resourceId/target",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createTarget),
|
||||
target.createTarget
|
||||
logActionAudit(ActionsEnum.createTarget),
|
||||
target.createTarget,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -289,7 +304,8 @@ authenticated.put(
|
||||
"/resource/:resourceId/rule",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createResourceRule),
|
||||
resource.createResourceRule
|
||||
logActionAudit(ActionsEnum.createResourceRule),
|
||||
resource.createResourceRule,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -303,14 +319,16 @@ authenticated.post(
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateResourceRule),
|
||||
resource.updateResourceRule
|
||||
logActionAudit(ActionsEnum.updateResourceRule),
|
||||
resource.updateResourceRule,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/resource/:resourceId/rule/:ruleId",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteResourceRule),
|
||||
resource.deleteResourceRule
|
||||
logActionAudit(ActionsEnum.deleteResourceRule),
|
||||
resource.deleteResourceRule,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -324,21 +342,24 @@ authenticated.post(
|
||||
"/target/:targetId",
|
||||
verifyApiKeyTargetAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateTarget),
|
||||
target.updateTarget
|
||||
logActionAudit(ActionsEnum.updateTarget),
|
||||
target.updateTarget,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/target/:targetId",
|
||||
verifyApiKeyTargetAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteTarget),
|
||||
target.deleteTarget
|
||||
logActionAudit(ActionsEnum.deleteTarget),
|
||||
target.deleteTarget,
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/role",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createRole),
|
||||
role.createRole
|
||||
logActionAudit(ActionsEnum.createRole),
|
||||
role.createRole,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -352,7 +373,8 @@ authenticated.delete(
|
||||
"/role/:roleId",
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteRole),
|
||||
role.deleteRole
|
||||
logActionAudit(ActionsEnum.deleteRole),
|
||||
role.deleteRole,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -367,7 +389,8 @@ authenticated.post(
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyApiKeyUserAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.addUserRole),
|
||||
user.addUserRole
|
||||
logActionAudit(ActionsEnum.addUserRole),
|
||||
user.addUserRole,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -375,7 +398,8 @@ authenticated.post(
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyRoleAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
|
||||
resource.setResourceRoles
|
||||
logActionAudit(ActionsEnum.setResourceRoles),
|
||||
resource.setResourceRoles,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -383,35 +407,40 @@ authenticated.post(
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeySetResourceUsers,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||
resource.setResourceUsers
|
||||
logActionAudit(ActionsEnum.setResourceUsers),
|
||||
resource.setResourceUsers,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/password`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePassword),
|
||||
resource.setResourcePassword
|
||||
logActionAudit(ActionsEnum.setResourcePassword),
|
||||
resource.setResourcePassword,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/pincode`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourcePincode),
|
||||
resource.setResourcePincode
|
||||
logActionAudit(ActionsEnum.setResourcePincode),
|
||||
resource.setResourcePincode,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/header-auth`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceHeaderAuth),
|
||||
resource.setResourceHeaderAuth
|
||||
logActionAudit(ActionsEnum.setResourceHeaderAuth),
|
||||
resource.setResourceHeaderAuth,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/resource/:resourceId/whitelist`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
|
||||
resource.setResourceWhitelist
|
||||
logActionAudit(ActionsEnum.setResourceWhitelist),
|
||||
resource.setResourceWhitelist,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -439,14 +468,16 @@ authenticated.post(
|
||||
`/resource/:resourceId/access-token`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.generateAccessToken),
|
||||
accessToken.generateAccessToken
|
||||
logActionAudit(ActionsEnum.generateAccessToken),
|
||||
accessToken.generateAccessToken,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
`/access-token/:accessTokenId`,
|
||||
verifyApiKeyAccessTokenAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteAcessToken),
|
||||
accessToken.deleteAccessToken
|
||||
logActionAudit(ActionsEnum.deleteAcessToken),
|
||||
accessToken.deleteAccessToken,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -474,7 +505,8 @@ authenticated.post(
|
||||
"/user/:userId/2fa",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateUser),
|
||||
user.updateUser2FA
|
||||
logActionAudit(ActionsEnum.updateUser),
|
||||
user.updateUser2FA,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -495,7 +527,8 @@ authenticated.put(
|
||||
"/org/:orgId/user",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createOrgUser),
|
||||
user.createOrgUser
|
||||
logActionAudit(ActionsEnum.createOrgUser),
|
||||
user.createOrgUser,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -503,7 +536,8 @@ authenticated.post(
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyUserAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateOrgUser),
|
||||
user.updateOrgUser
|
||||
logActionAudit(ActionsEnum.updateOrgUser),
|
||||
user.updateOrgUser,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -511,7 +545,8 @@ authenticated.delete(
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyUserAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.removeUser),
|
||||
user.removeUserOrg
|
||||
logActionAudit(ActionsEnum.removeUser),
|
||||
user.removeUserOrg,
|
||||
);
|
||||
|
||||
// authenticated.put(
|
||||
@@ -531,7 +566,8 @@ authenticated.post(
|
||||
`/org/:orgId/api-key/:apiKeyId/actions`,
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.setApiKeyActions),
|
||||
apiKeys.setApiKeyActions
|
||||
logActionAudit(ActionsEnum.setApiKeyActions),
|
||||
apiKeys.setApiKeyActions,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -545,28 +581,32 @@ authenticated.put(
|
||||
`/org/:orgId/api-key`,
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.createApiKey),
|
||||
apiKeys.createOrgApiKey
|
||||
logActionAudit(ActionsEnum.createApiKey),
|
||||
apiKeys.createOrgApiKey,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
`/org/:orgId/api-key/:apiKeyId`,
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteApiKey),
|
||||
apiKeys.deleteApiKey
|
||||
logActionAudit(ActionsEnum.deleteApiKey),
|
||||
apiKeys.deleteApiKey,
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/idp/oidc",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.createIdp),
|
||||
idp.createOidcIdp
|
||||
logActionAudit(ActionsEnum.createIdp),
|
||||
idp.createOidcIdp,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/idp/:idpId/oidc",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateIdp),
|
||||
idp.updateOidcIdp
|
||||
logActionAudit(ActionsEnum.updateIdp),
|
||||
idp.updateOidcIdp,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -587,21 +627,24 @@ authenticated.put(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.createIdpOrg),
|
||||
idp.createIdpOrgPolicy
|
||||
logActionAudit(ActionsEnum.createIdpOrg),
|
||||
idp.createIdpOrgPolicy,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateIdpOrg),
|
||||
idp.updateIdpOrgPolicy
|
||||
logActionAudit(ActionsEnum.updateIdpOrg),
|
||||
idp.updateIdpOrgPolicy,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyApiKeyIsRoot,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteIdpOrg),
|
||||
idp.deleteIdpOrgPolicy
|
||||
logActionAudit(ActionsEnum.deleteIdpOrg),
|
||||
idp.deleteIdpOrgPolicy,
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
@@ -640,7 +683,8 @@ authenticated.put(
|
||||
verifyClientsEnabled,
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.createClient),
|
||||
client.createClient
|
||||
logActionAudit(ActionsEnum.createClient),
|
||||
client.createClient,
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
@@ -648,7 +692,8 @@ authenticated.delete(
|
||||
verifyClientsEnabled,
|
||||
verifyApiKeyClientAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.deleteClient),
|
||||
client.deleteClient
|
||||
logActionAudit(ActionsEnum.deleteClient),
|
||||
client.deleteClient,
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
@@ -656,12 +701,14 @@ authenticated.post(
|
||||
verifyClientsEnabled,
|
||||
verifyApiKeyClientAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateClient),
|
||||
client.updateClient
|
||||
logActionAudit(ActionsEnum.updateClient),
|
||||
client.updateClient,
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/blueprint",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.applyBlueprint),
|
||||
org.applyBlueprint
|
||||
logActionAudit(ActionsEnum.applyBlueprint),
|
||||
org.applyBlueprint,
|
||||
);
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import NodeCache from "node-cache";
|
||||
import { sendToClient } from "#dynamic/routers/ws";
|
||||
|
||||
export const dockerSocketCache = new NodeCache({
|
||||
stdTTL: 3600 // seconds
|
||||
});
|
||||
|
||||
export function fetchContainers(newtId: string) {
|
||||
const payload = {
|
||||
type: `newt/socket/fetch`,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import logger from "@server/logger";
|
||||
import { dockerSocketCache } from "./dockerSocket";
|
||||
import { Newt } from "@server/db";
|
||||
import { applyNewtDockerBlueprint } from "@server/lib/blueprints/applyNewtDockerBlueprint";
|
||||
import cache from "@server/lib/cache";
|
||||
|
||||
export const handleDockerStatusMessage: MessageHandler = async (context) => {
|
||||
const { message, client, sendToClient } = context;
|
||||
@@ -24,8 +24,8 @@ export const handleDockerStatusMessage: MessageHandler = async (context) => {
|
||||
|
||||
if (available) {
|
||||
logger.info(`Newt ${newt.newtId} has Docker socket access`);
|
||||
dockerSocketCache.set(`${newt.newtId}:socketPath`, socketPath, 0);
|
||||
dockerSocketCache.set(`${newt.newtId}:isAvailable`, available, 0);
|
||||
cache.set(`${newt.newtId}:socketPath`, socketPath, 0);
|
||||
cache.set(`${newt.newtId}:isAvailable`, available, 0);
|
||||
} else {
|
||||
logger.warn(`Newt ${newt.newtId} does not have Docker socket access`);
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export const handleDockerContainersMessage: MessageHandler = async (
|
||||
);
|
||||
|
||||
if (containers && containers.length > 0) {
|
||||
dockerSocketCache.set(`${newt.newtId}:dockerContainers`, containers, 0);
|
||||
cache.set(`${newt.newtId}:dockerContainers`, containers, 0);
|
||||
} else {
|
||||
logger.warn(`Newt ${newt.newtId} does not have Docker containers`);
|
||||
}
|
||||
|
||||
@@ -49,13 +49,13 @@ export async function getOrg(
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const org = await db
|
||||
const [org] = await db
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.limit(1);
|
||||
|
||||
if (org.length === 0) {
|
||||
if (!org) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
@@ -66,7 +66,7 @@ export async function getOrg(
|
||||
|
||||
return response<GetOrgResponse>(res, {
|
||||
data: {
|
||||
org: org[0]
|
||||
org
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
|
||||
@@ -26,7 +26,19 @@ const updateOrgBodySchema = z
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
requireTwoFactor: z.boolean().optional(),
|
||||
maxSessionLengthHours: z.number().nullable().optional(),
|
||||
passwordExpiryDays: z.number().nullable().optional()
|
||||
passwordExpiryDays: z.number().nullable().optional(),
|
||||
settingsLogRetentionDaysRequest: z
|
||||
.number()
|
||||
.min(build === "saas" ? 0 : -1)
|
||||
.optional(),
|
||||
settingsLogRetentionDaysAccess: z
|
||||
.number()
|
||||
.min(build === "saas" ? 0 : -1)
|
||||
.optional(),
|
||||
settingsLogRetentionDaysAction: z
|
||||
.number()
|
||||
.min(build === "saas" ? 0 : -1)
|
||||
.optional()
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
@@ -86,13 +98,32 @@ export async function updateOrg(
|
||||
parsedBody.data.passwordExpiryDays = undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
!isLicensed &&
|
||||
parsedBody.data.settingsLogRetentionDaysRequest &&
|
||||
parsedBody.data.settingsLogRetentionDaysRequest > 30
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"You are not allowed to set log retention days greater than 30 because you are not subscribed to the Standard tier"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const updatedOrg = await db
|
||||
.update(orgs)
|
||||
.set({
|
||||
name: parsedBody.data.name,
|
||||
requireTwoFactor: parsedBody.data.requireTwoFactor,
|
||||
maxSessionLengthHours: parsedBody.data.maxSessionLengthHours,
|
||||
passwordExpiryDays: parsedBody.data.passwordExpiryDays
|
||||
passwordExpiryDays: parsedBody.data.passwordExpiryDays,
|
||||
settingsLogRetentionDaysRequest:
|
||||
parsedBody.data.settingsLogRetentionDaysRequest,
|
||||
settingsLogRetentionDaysAccess:
|
||||
parsedBody.data.settingsLogRetentionDaysAccess,
|
||||
settingsLogRetentionDaysAction:
|
||||
parsedBody.data.settingsLogRetentionDaysAction
|
||||
})
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.returning();
|
||||
|
||||
@@ -10,11 +10,10 @@ import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { createResourceSession } from "@server/auth/sessions/resource";
|
||||
import logger from "@server/logger";
|
||||
import {
|
||||
verifyResourceAccessToken
|
||||
} from "@server/auth/verifyResourceAccessToken";
|
||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||
import config from "@server/lib/config";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { logAccessAudit } from "@server/private/lib/logAccessAudit";
|
||||
|
||||
const authWithAccessTokenBodySchema = z
|
||||
.object({
|
||||
@@ -131,6 +130,16 @@ export async function authWithAccessToken(
|
||||
`Resource access token invalid. Resource ID: ${resource.resourceId}. IP: ${req.ip}.`
|
||||
);
|
||||
}
|
||||
|
||||
logAccessAudit({
|
||||
orgId: resource.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: false,
|
||||
type: "accessToken",
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
@@ -150,6 +159,15 @@ export async function authWithAccessToken(
|
||||
doNotExtend: true
|
||||
});
|
||||
|
||||
logAccessAudit({
|
||||
orgId: resource.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: true,
|
||||
type: "accessToken",
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
|
||||
return response<AuthWithAccessTokenResponse>(res, {
|
||||
data: {
|
||||
session: token,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { createResourceSession } from "@server/auth/sessions/resource";
|
||||
import logger from "@server/logger";
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
import config from "@server/lib/config";
|
||||
import { logAccessAudit } from "@server/private/lib/logAccessAudit";
|
||||
|
||||
export const authWithPasswordBodySchema = z
|
||||
.object({
|
||||
@@ -113,6 +114,16 @@ export async function authWithPassword(
|
||||
`Resource password incorrect. Resource ID: ${resource.resourceId}. IP: ${req.ip}.`
|
||||
);
|
||||
}
|
||||
|
||||
logAccessAudit({
|
||||
orgId: org.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: false,
|
||||
type: "password",
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Incorrect password")
|
||||
);
|
||||
@@ -129,6 +140,15 @@ export async function authWithPassword(
|
||||
doNotExtend: true
|
||||
});
|
||||
|
||||
logAccessAudit({
|
||||
orgId: org.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: true,
|
||||
type: "password",
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
|
||||
return response<AuthWithPasswordResponse>(res, {
|
||||
data: {
|
||||
session: token
|
||||
|
||||
@@ -12,6 +12,7 @@ import { createResourceSession } from "@server/auth/sessions/resource";
|
||||
import logger from "@server/logger";
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
import config from "@server/lib/config";
|
||||
import { logAccessAudit } from "@server/private/lib/logAccessAudit";
|
||||
|
||||
export const authWithPincodeBodySchema = z
|
||||
.object({
|
||||
@@ -112,6 +113,16 @@ export async function authWithPincode(
|
||||
`Resource pin code incorrect. Resource ID: ${resource.resourceId}. IP: ${req.ip}.`
|
||||
);
|
||||
}
|
||||
|
||||
logAccessAudit({
|
||||
orgId: org.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: false,
|
||||
type: "pincode",
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "Incorrect PIN")
|
||||
);
|
||||
@@ -128,6 +139,15 @@ export async function authWithPincode(
|
||||
doNotExtend: true
|
||||
});
|
||||
|
||||
logAccessAudit({
|
||||
orgId: org.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: true,
|
||||
type: "pincode",
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
|
||||
return response<AuthWithPincodeResponse>(res, {
|
||||
data: {
|
||||
session: token
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import { db } from "@server/db";
|
||||
import {
|
||||
orgs,
|
||||
resourceOtp,
|
||||
resources,
|
||||
resourceWhitelist
|
||||
} from "@server/db";
|
||||
import { orgs, resourceOtp, resources, resourceWhitelist } from "@server/db";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/lib/response";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
@@ -17,13 +12,11 @@ import { createResourceSession } from "@server/auth/sessions/resource";
|
||||
import { isValidOtp, sendResourceOtpEmail } from "@server/auth/resourceOtp";
|
||||
import logger from "@server/logger";
|
||||
import config from "@server/lib/config";
|
||||
import { logAccessAudit } from "@server/private/lib/logAccessAudit";
|
||||
|
||||
const authWithWhitelistBodySchema = z
|
||||
.object({
|
||||
email: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.email(),
|
||||
email: z.string().toLowerCase().email(),
|
||||
otp: z.string().optional()
|
||||
})
|
||||
.strict();
|
||||
@@ -126,6 +119,19 @@ export async function authWithWhitelist(
|
||||
`Email is not whitelisted. Email: ${email}. IP: ${req.ip}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (org && resource) {
|
||||
logAccessAudit({
|
||||
orgId: org.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: false,
|
||||
type: "whitelistedEmail",
|
||||
metadata: { email },
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
}
|
||||
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.UNAUTHORIZED,
|
||||
@@ -219,6 +225,16 @@ export async function authWithWhitelist(
|
||||
doNotExtend: true
|
||||
});
|
||||
|
||||
logAccessAudit({
|
||||
orgId: org.orgId,
|
||||
resourceId: resource.resourceId,
|
||||
action: true,
|
||||
metadata: { email },
|
||||
type: "whitelistedEmail",
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
|
||||
return response<AuthWithWhitelistResponse>(res, {
|
||||
data: {
|
||||
session: token
|
||||
|
||||
@@ -18,7 +18,7 @@ import { OpenAPITags, registry } from "@server/openApi";
|
||||
const createResourceRuleSchema = z
|
||||
.object({
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "GEOIP"]),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "COUNTRY"]),
|
||||
value: z.string().min(1),
|
||||
priority: z.number().int(),
|
||||
enabled: z.boolean().optional()
|
||||
|
||||
@@ -14,6 +14,7 @@ import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { response } from "@server/lib/response";
|
||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||
import { logAccessAudit } from "#private/lib/logAccessAudit";
|
||||
|
||||
const getExchangeTokenParams = z
|
||||
.object({
|
||||
@@ -46,13 +47,13 @@ export async function getExchangeToken(
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
const resource = await db
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (resource.length === 0) {
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
@@ -105,6 +106,21 @@ export async function getExchangeToken(
|
||||
doNotExtend: true
|
||||
});
|
||||
|
||||
if (req.user) {
|
||||
logAccessAudit({
|
||||
orgId: resource.orgId,
|
||||
resourceId: resourceId,
|
||||
user: {
|
||||
username: req.user.username,
|
||||
userId: req.user.userId
|
||||
},
|
||||
action: true,
|
||||
type: "login",
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug("Request token created successfully");
|
||||
|
||||
return response<GetExchangeTokenResponse>(res, {
|
||||
|
||||
@@ -99,8 +99,9 @@ const updateRawResourceBodySchema = z
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
proxyPort: z.number().int().min(1).max(65535).optional(),
|
||||
stickySession: z.boolean().optional(),
|
||||
enabled: z.boolean().optional()
|
||||
// enableProxy: z.boolean().optional() // always true now
|
||||
enabled: z.boolean().optional(),
|
||||
proxyProtocol: z.boolean().optional(),
|
||||
proxyProtocolVersion: z.number().int().min(1).optional()
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
|
||||
@@ -30,7 +30,7 @@ const updateResourceRuleParamsSchema = z
|
||||
const updateResourceRuleSchema = z
|
||||
.object({
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]).optional(),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "GEOIP"]).optional(),
|
||||
match: z.enum(["CIDR", "IP", "PATH", "COUNTRY"]).optional(),
|
||||
value: z.string().min(1).optional(),
|
||||
priority: z.number().int(),
|
||||
enabled: z.boolean().optional()
|
||||
|
||||
@@ -9,14 +9,12 @@ import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import NodeCache from "node-cache";
|
||||
import semver from "semver";
|
||||
|
||||
const newtVersionCache = new NodeCache({ stdTTL: 3600 }); // 1 hours in seconds
|
||||
import cache from "@server/lib/cache";
|
||||
|
||||
async function getLatestNewtVersion(): Promise<string | null> {
|
||||
try {
|
||||
const cachedVersion = newtVersionCache.get<string>("latestNewtVersion");
|
||||
const cachedVersion = cache.get<string>("latestNewtVersion");
|
||||
if (cachedVersion) {
|
||||
return cachedVersion;
|
||||
}
|
||||
@@ -48,7 +46,7 @@ async function getLatestNewtVersion(): Promise<string | null> {
|
||||
|
||||
const latestVersion = tags[0].name;
|
||||
|
||||
newtVersionCache.set("latestNewtVersion", latestVersion);
|
||||
cache.set("latestNewtVersion", latestVersion);
|
||||
|
||||
return latestVersion;
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -12,9 +12,9 @@ import stoi from "@server/lib/stoi";
|
||||
import { sendToClient } from "#dynamic/routers/ws";
|
||||
import {
|
||||
fetchContainers,
|
||||
dockerSocketCache,
|
||||
dockerSocket
|
||||
} from "../newt/dockerSocket";
|
||||
import cache from "@server/lib/cache";
|
||||
|
||||
export interface ContainerNetwork {
|
||||
networkId: string;
|
||||
@@ -157,7 +157,7 @@ async function triggerFetch(siteId: number) {
|
||||
|
||||
// clear the cache for this Newt ID so that the site has to keep asking for the containers
|
||||
// this is to ensure that the site always gets the latest data
|
||||
dockerSocketCache.del(`${newt.newtId}:dockerContainers`);
|
||||
cache.del(`${newt.newtId}:dockerContainers`);
|
||||
|
||||
return { siteId, newtId: newt.newtId };
|
||||
}
|
||||
@@ -165,7 +165,7 @@ async function triggerFetch(siteId: number) {
|
||||
async function queryContainers(siteId: number) {
|
||||
const { newt } = await getSiteAndNewt(siteId);
|
||||
|
||||
const result = dockerSocketCache.get(
|
||||
const result = cache.get(
|
||||
`${newt.newtId}:dockerContainers`
|
||||
) as Container[];
|
||||
if (!result) {
|
||||
@@ -182,7 +182,7 @@ async function isDockerAvailable(siteId: number): Promise<boolean> {
|
||||
const { newt } = await getSiteAndNewt(siteId);
|
||||
|
||||
const key = `${newt.newtId}:isAvailable`;
|
||||
const isAvailable = dockerSocketCache.get(key);
|
||||
const isAvailable = cache.get(key);
|
||||
|
||||
return !!isAvailable;
|
||||
}
|
||||
@@ -196,8 +196,8 @@ async function getDockerStatus(
|
||||
const mappedKeys = keys.map((x) => `${newt.newtId}:${x}`);
|
||||
|
||||
const result = {
|
||||
isAvailable: dockerSocketCache.get(mappedKeys[0]) as boolean,
|
||||
socketPath: dockerSocketCache.get(mappedKeys[1]) as string | undefined
|
||||
isAvailable: cache.get(mappedKeys[0]) as boolean,
|
||||
socketPath: cache.get(mappedKeys[1]) as string | undefined
|
||||
};
|
||||
|
||||
return result;
|
||||
|
||||
@@ -21,7 +21,8 @@ export async function traefikConfigProvider(
|
||||
currentExitNodeId,
|
||||
config.getRawConfig().traefik.site_types,
|
||||
build == "oss", // filter out the namespace domains in open source
|
||||
build != "oss" // generate the login pages on the cloud and hybrid
|
||||
build != "oss", // generate the login pages on the cloud and and enterprise,
|
||||
config.getRawConfig().traefik.allow_raw_resources
|
||||
);
|
||||
|
||||
if (traefikConfig?.http?.middlewares) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import NodeCache from "node-cache";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
@@ -20,8 +19,7 @@ import { UserType } from "@server/types/UserTypes";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import { FeatureId } from "@server/lib/billing";
|
||||
import { build } from "@server/build";
|
||||
|
||||
const regenerateTracker = new NodeCache({ stdTTL: 3600, checkperiod: 600 });
|
||||
import cache from "@server/lib/cache";
|
||||
|
||||
const inviteUserParamsSchema = z
|
||||
.object({
|
||||
@@ -182,7 +180,7 @@ export async function inviteUser(
|
||||
}
|
||||
|
||||
if (existingInvite.length) {
|
||||
const attempts = regenerateTracker.get<number>(email) || 0;
|
||||
const attempts = cache.get<number>(email) || 0;
|
||||
if (attempts >= 3) {
|
||||
return next(
|
||||
createHttpError(
|
||||
@@ -192,7 +190,7 @@ export async function inviteUser(
|
||||
);
|
||||
}
|
||||
|
||||
regenerateTracker.set(email, attempts + 1);
|
||||
cache.set(email, attempts + 1);
|
||||
|
||||
const inviteId = existingInvite[0].inviteId; // Retrieve the original inviteId
|
||||
const token = generateRandomString(
|
||||
|
||||
Reference in New Issue
Block a user