Merge branch 'dev' into msg-delivery

This commit is contained in:
Owen
2026-01-16 12:22:23 -08:00
83 changed files with 4448 additions and 767 deletions

View File

@@ -1,6 +1,6 @@
import { NextFunction, Request, Response } from "express";
import { db } from "@server/db";
import { olms } from "@server/db";
import { olms, clients, fingerprints } from "@server/db";
import { eq, and } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -9,6 +9,7 @@ import { z } from "zod";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
import { getUserDeviceName } from "@server/db/names";
const paramsSchema = z
.object({
@@ -17,6 +18,10 @@ const paramsSchema = z
})
.strict();
const querySchema = z.object({
orgId: z.string().optional()
});
// registry.registerPath({
// method: "get",
// path: "/user/{userId}/olm/{olmId}",
@@ -44,15 +49,64 @@ export async function getUserOlm(
);
}
const { olmId, userId } = parsedParams.data;
const parsedQuery = querySchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const [olm] = await db
const { olmId, userId } = parsedParams.data;
const { orgId } = parsedQuery.data;
const [result] = await db
.select()
.from(olms)
.where(and(eq(olms.userId, userId), eq(olms.olmId, olmId)));
.where(and(eq(olms.userId, userId), eq(olms.olmId, olmId)))
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
.limit(1);
if (!result || !result.olms) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Olm not found"
)
);
}
const olm = result.olms;
// If orgId is provided and olm has a clientId, fetch the client to check blocked status
let blocked: boolean | undefined;
if (orgId && olm.clientId) {
const [client] = await db
.select({ blocked: clients.blocked })
.from(clients)
.where(
and(
eq(clients.clientId, olm.clientId),
eq(clients.orgId, orgId)
)
)
.limit(1);
blocked = client?.blocked ?? false;
}
// Replace name with device name
const model = result.fingerprints?.deviceModel || null;
const newName = getUserDeviceName(model, olm.name);
const responseData = blocked !== undefined
? { ...olm, name: newName, blocked }
: { ...olm, name: newName };
return response(res, {
data: olm,
data: responseData,
success: true,
error: false,
message: "Successfully retrieved olm",

View File

@@ -1,5 +1,5 @@
import { db } from "@server/db";
import { disconnectClient, getClientConfigVersion } from "#dynamic/routers/ws";
import { clientPostureSnapshots, db, fingerprints } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { clients, olms, Olm } from "@server/db";
import { eq, lt, isNull, and, or } from "drizzle-orm";
@@ -102,7 +102,7 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
const { message, client: c, sendToClient } = context;
const olm = c as Olm;
const { userToken } = message.data;
const { userToken, fingerprint, postures } = message.data;
if (!olm) {
logger.warn("Olm not found");
@@ -206,6 +206,74 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
logger.error("Error handling ping message", { error });
}
const now = Math.floor(Date.now() / 1000);
if (fingerprint && olm.olmId) {
const [existingFingerprint] = await db
.select()
.from(fingerprints)
.where(eq(fingerprints.olmId, olm.olmId))
.limit(1);
if (!existingFingerprint) {
await db.insert(fingerprints).values({
olmId: olm.olmId,
firstSeen: now,
lastSeen: now,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint
});
} else {
await db
.update(fingerprints)
.set({
lastSeen: now,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint
})
.where(eq(fingerprints.olmId, olm.olmId));
}
}
if (postures && olm.clientId) {
await db.insert(clientPostureSnapshots).values({
clientId: olm.clientId,
biometricsEnabled: postures?.biometricsEnabled,
diskEncrypted: postures?.diskEncrypted,
firewallEnabled: postures?.firewallEnabled,
autoUpdatesEnabled: postures?.autoUpdatesEnabled,
tpmAvailable: postures?.tpmAvailable,
windowsDefenderEnabled: postures?.windowsDefenderEnabled,
macosSipEnabled: postures?.macosSipEnabled,
macosGatekeeperEnabled: postures?.macosGatekeeperEnabled,
macosFirewallStealthMode: postures?.macosFirewallStealthMode,
linuxAppArmorEnabled: postures?.linuxAppArmorEnabled,
linuxSELinuxEnabled: postures?.linuxSELinuxEnabled,
collectedAt: now
});
}
return {
message: {
type: "pong",

View File

@@ -1,7 +1,9 @@
import {
Client,
clientPostureSnapshots,
clientSiteResourcesAssociationsCache,
db,
fingerprints,
orgs,
siteResources
} from "@server/db";
@@ -38,8 +40,16 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return;
}
const { publicKey, relay, olmVersion, olmAgent, orgId, userToken } =
message.data;
const {
publicKey,
relay,
olmVersion,
olmAgent,
orgId,
userToken,
fingerprint,
postures
} = message.data;
if (!olm.clientId) {
logger.warn("Olm client ID not found");
@@ -188,6 +198,72 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
relay
);
if (fingerprint) {
const [existingFingerprint] = await db
.select()
.from(fingerprints)
.where(eq(fingerprints.olmId, olm.olmId))
.limit(1);
if (!existingFingerprint) {
await db.insert(fingerprints).values({
olmId: olm.olmId,
firstSeen: now,
lastSeen: now,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint
});
} else {
await db
.update(fingerprints)
.set({
lastSeen: now,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint
})
.where(eq(fingerprints.olmId, olm.olmId));
}
}
if (postures && olm.clientId) {
await db.insert(clientPostureSnapshots).values({
clientId: olm.clientId,
biometricsEnabled: postures?.biometricsEnabled,
diskEncrypted: postures?.diskEncrypted,
firewallEnabled: postures?.firewallEnabled,
autoUpdatesEnabled: postures?.autoUpdatesEnabled,
tpmAvailable: postures?.tpmAvailable,
windowsDefenderEnabled: postures?.windowsDefenderEnabled,
macosSipEnabled: postures?.macosSipEnabled,
macosGatekeeperEnabled: postures?.macosGatekeeperEnabled,
macosFirewallStealthMode: postures?.macosFirewallStealthMode,
linuxAppArmorEnabled: postures?.linuxAppArmorEnabled,
linuxSELinuxEnabled: postures?.linuxSELinuxEnabled,
collectedAt: now
});
}
// REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES
// if (siteConfigurations.length === 0) {
// logger.warn("No valid site configurations found");

View File

@@ -9,3 +9,4 @@ export * from "./listUserOlms";
export * from "./getUserOlm";
export * from "./handleOlmServerPeerAddMessage";
export * from "./handleOlmUnRelayMessage";
export * from "./recoverOlmWithFingerprint";

View File

@@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from "express";
import { db } from "@server/db";
import { db, fingerprints } from "@server/db";
import { olms } from "@server/db";
import { eq, count, desc } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode";
@@ -9,6 +9,7 @@ import { z } from "zod";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
import { getUserDeviceName } from "@server/db/names";
const querySchema = z.object({
limit: z
@@ -99,22 +100,30 @@ export async function listUserOlms(
const total = totalCountResult?.count || 0;
// Get OLMs for the current user (including archived OLMs)
const userOlms = await db
.select({
olmId: olms.olmId,
dateCreated: olms.dateCreated,
version: olms.version,
name: olms.name,
clientId: olms.clientId,
userId: olms.userId,
archived: olms.archived
})
const list = await db
.select()
.from(olms)
.where(eq(olms.userId, userId))
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
.orderBy(desc(olms.dateCreated))
.limit(limit)
.offset(offset);
const userOlms = list.map((item) => {
const model = item.fingerprints?.deviceModel || null;
const newName = getUserDeviceName(model, item.olms.name);
return {
olmId: item.olms.olmId,
dateCreated: item.olms.dateCreated,
version: item.olms.version,
name: newName,
clientId: item.olms.clientId,
userId: item.olms.userId,
archived: item.olms.archived
};
});
return response<ListUserOlmsResponse>(res, {
data: {
olms: userOlms,

View File

@@ -0,0 +1,120 @@
import { db, fingerprints, olms } from "@server/db";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import { and, eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import response from "@server/lib/response";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { generateId } from "@server/auth/sessions/app";
import { hashPassword } from "@server/auth/password";
const paramsSchema = z
.object({
userId: z.string()
})
.strict();
const bodySchema = z
.object({
platformFingerprint: z.string()
})
.strict();
export async function recoverOlmWithFingerprint(
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 { userId } = parsedParams.data;
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { platformFingerprint } = parsedBody.data;
const result = await db
.select({
olm: olms,
fingerprint: fingerprints
})
.from(olms)
.innerJoin(fingerprints, eq(fingerprints.olmId, olms.olmId))
.where(
and(
eq(olms.userId, userId),
eq(olms.archived, false),
eq(fingerprints.platformFingerprint, platformFingerprint)
)
)
.orderBy(fingerprints.lastSeen);
if (!result || result.length == 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"corresponding olm with this fingerprint not found"
)
);
}
if (result.length > 1) {
return next(
createHttpError(
HttpCode.CONFLICT,
"multiple matching fingerprints found, not resetting secrets"
)
);
}
const [{ olm: foundOlm }] = result;
const newSecret = generateId(48);
const newSecretHash = await hashPassword(newSecret);
await db
.update(olms)
.set({
secretHash: newSecretHash
})
.where(eq(olms.olmId, foundOlm.olmId));
return response(res, {
data: {
olmId: foundOlm.olmId,
secret: newSecret
},
success: true,
error: false,
message: "Successfully retrieved olm",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to recover olm using provided fingerprint input"
)
);
}
}