mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Merge branch 'dev' into msg-delivery
This commit is contained in:
@@ -16,4 +16,5 @@ export * from "./checkResourceSession";
|
||||
export * from "./securityKey";
|
||||
export * from "./startDeviceWebAuth";
|
||||
export * from "./verifyDeviceWebAuth";
|
||||
export * from "./pollDeviceWebAuth";
|
||||
export * from "./pollDeviceWebAuth";
|
||||
export * from "./lookupUser";
|
||||
224
server/routers/auth/lookupUser.ts
Normal file
224
server/routers/auth/lookupUser.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import {
|
||||
users,
|
||||
userOrgs,
|
||||
orgs,
|
||||
idpOrg,
|
||||
idp,
|
||||
idpOidcConfig
|
||||
} from "@server/db";
|
||||
import { eq, or, sql, and, isNotNull, inArray } 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 { UserType } from "@server/types/UserTypes";
|
||||
|
||||
const lookupBodySchema = z.strictObject({
|
||||
identifier: z.string().min(1).toLowerCase()
|
||||
});
|
||||
|
||||
export type LookupUserResponse = {
|
||||
found: boolean;
|
||||
identifier: string;
|
||||
accounts: Array<{
|
||||
userId: string;
|
||||
email: string | null;
|
||||
username: string;
|
||||
hasInternalAuth: boolean;
|
||||
orgs: Array<{
|
||||
orgId: string;
|
||||
orgName: string;
|
||||
idps: Array<{
|
||||
idpId: number;
|
||||
name: string;
|
||||
variant: string | null;
|
||||
}>;
|
||||
hasInternalAuth: boolean;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
|
||||
// registry.registerPath({
|
||||
// method: "post",
|
||||
// path: "/auth/lookup-user",
|
||||
// description: "Lookup user accounts by username or email and return available authentication methods.",
|
||||
// tags: [OpenAPITags.Auth],
|
||||
// request: {
|
||||
// body: lookupBodySchema
|
||||
// },
|
||||
// responses: {}
|
||||
// });
|
||||
|
||||
export async function lookupUser(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = lookupBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { identifier } = parsedBody.data;
|
||||
|
||||
// Query users matching identifier (case-insensitive)
|
||||
// Match by username OR email
|
||||
const matchingUsers = await db
|
||||
.select({
|
||||
userId: users.userId,
|
||||
email: users.email,
|
||||
username: users.username,
|
||||
type: users.type,
|
||||
passwordHash: users.passwordHash,
|
||||
idpId: users.idpId
|
||||
})
|
||||
.from(users)
|
||||
.where(
|
||||
or(
|
||||
sql`LOWER(${users.username}) = ${identifier}`,
|
||||
sql`LOWER(${users.email}) = ${identifier}`
|
||||
)
|
||||
);
|
||||
|
||||
if (!matchingUsers || matchingUsers.length === 0) {
|
||||
return response<LookupUserResponse>(res, {
|
||||
data: {
|
||||
found: false,
|
||||
identifier,
|
||||
accounts: []
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "No accounts found",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
}
|
||||
|
||||
// Get unique user IDs
|
||||
const userIds = [...new Set(matchingUsers.map((u) => u.userId))];
|
||||
|
||||
// Get all org memberships for these users
|
||||
const orgMemberships = await db
|
||||
.select({
|
||||
userId: userOrgs.userId,
|
||||
orgId: userOrgs.orgId,
|
||||
orgName: orgs.name
|
||||
})
|
||||
.from(userOrgs)
|
||||
.innerJoin(orgs, eq(orgs.orgId, userOrgs.orgId))
|
||||
.where(inArray(userOrgs.userId, userIds));
|
||||
|
||||
// Get unique org IDs
|
||||
const orgIds = [...new Set(orgMemberships.map((m) => m.orgId))];
|
||||
|
||||
// Get all IdPs for these orgs
|
||||
const orgIdps =
|
||||
orgIds.length > 0
|
||||
? await db
|
||||
.select({
|
||||
orgId: idpOrg.orgId,
|
||||
idpId: idp.idpId,
|
||||
idpName: idp.name,
|
||||
variant: idpOidcConfig.variant
|
||||
})
|
||||
.from(idpOrg)
|
||||
.innerJoin(idp, eq(idp.idpId, idpOrg.idpId))
|
||||
.innerJoin(
|
||||
idpOidcConfig,
|
||||
eq(idpOidcConfig.idpId, idp.idpId)
|
||||
)
|
||||
.where(inArray(idpOrg.orgId, orgIds))
|
||||
: [];
|
||||
|
||||
// Build response structure
|
||||
const accounts: LookupUserResponse["accounts"] = [];
|
||||
|
||||
for (const user of matchingUsers) {
|
||||
const hasInternalAuth =
|
||||
user.type === UserType.Internal && user.passwordHash !== null;
|
||||
|
||||
// Get orgs for this user
|
||||
const userOrgMemberships = orgMemberships.filter(
|
||||
(m) => m.userId === user.userId
|
||||
);
|
||||
|
||||
// Deduplicate orgs (user might have multiple memberships in same org)
|
||||
const uniqueOrgs = new Map<string, typeof userOrgMemberships[0]>();
|
||||
for (const membership of userOrgMemberships) {
|
||||
if (!uniqueOrgs.has(membership.orgId)) {
|
||||
uniqueOrgs.set(membership.orgId, membership);
|
||||
}
|
||||
}
|
||||
|
||||
const orgsData = Array.from(uniqueOrgs.values()).map((membership) => {
|
||||
// Get IdPs for this org where the user (with the exact identifier) is authenticated via that IdP
|
||||
// Only show IdPs where the user's idpId matches
|
||||
// Internal users don't have an idpId, so they won't see any IdPs
|
||||
const orgIdpsList = orgIdps
|
||||
.filter((idp) => {
|
||||
if (idp.orgId !== membership.orgId) {
|
||||
return false;
|
||||
}
|
||||
// Only show IdPs where the user (with exact identifier) is authenticated via that IdP
|
||||
// This means user.idpId must match idp.idpId
|
||||
if (user.idpId !== null && user.idpId === idp.idpId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map((idp) => ({
|
||||
idpId: idp.idpId,
|
||||
name: idp.idpName,
|
||||
variant: idp.variant
|
||||
}));
|
||||
|
||||
// Check if user has internal auth for this org
|
||||
// User has internal auth if they have an internal account type
|
||||
const orgHasInternalAuth = hasInternalAuth;
|
||||
|
||||
return {
|
||||
orgId: membership.orgId,
|
||||
orgName: membership.orgName,
|
||||
idps: orgIdpsList,
|
||||
hasInternalAuth: orgHasInternalAuth
|
||||
};
|
||||
});
|
||||
|
||||
accounts.push({
|
||||
userId: user.userId,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
hasInternalAuth,
|
||||
orgs: orgsData
|
||||
});
|
||||
}
|
||||
|
||||
return response<LookupUserResponse>(res, {
|
||||
data: {
|
||||
found: true,
|
||||
identifier,
|
||||
accounts
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "User lookup completed",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { eq, and, gt } from "drizzle-orm";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { unauthorized } from "@server/auth/unauthorizedResponse";
|
||||
import { getIosDeviceName, getMacDeviceName } from "@server/db/names";
|
||||
|
||||
const bodySchema = z
|
||||
.object({
|
||||
@@ -120,6 +121,11 @@ export async function verifyDeviceWebAuth(
|
||||
);
|
||||
}
|
||||
|
||||
const deviceName =
|
||||
getMacDeviceName(deviceCode.deviceName) ||
|
||||
getIosDeviceName(deviceCode.deviceName) ||
|
||||
deviceCode.deviceName;
|
||||
|
||||
// If verify is false, just return metadata without verifying
|
||||
if (!verify) {
|
||||
return response<VerifyDeviceWebAuthResponse>(res, {
|
||||
@@ -129,7 +135,7 @@ export async function verifyDeviceWebAuth(
|
||||
metadata: {
|
||||
ip: deviceCode.ip,
|
||||
city: deviceCode.city,
|
||||
deviceName: deviceCode.deviceName,
|
||||
deviceName: deviceName,
|
||||
applicationName: deviceCode.applicationName,
|
||||
createdAt: deviceCode.createdAt
|
||||
}
|
||||
|
||||
@@ -942,7 +942,7 @@ async function isUserAllowedToAccessResource(
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
role: userOrgRole.roleName
|
||||
};
|
||||
}
|
||||
|
||||
@@ -956,7 +956,7 @@ async function isUserAllowedToAccessResource(
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
role: userOrgRole.roleName
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ export async function blockClient(
|
||||
// Block the client
|
||||
await trx
|
||||
.update(clients)
|
||||
.set({ blocked: true })
|
||||
.set({ blocked: true, approvalState: "denied" })
|
||||
.where(eq(clients.clientId, clientId));
|
||||
|
||||
// Send terminate signal if there's an associated OLM and it's connected
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, olms } from "@server/db";
|
||||
import { clients } from "@server/db";
|
||||
import { clients, fingerprints } from "@server/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -10,6 +10,7 @@ import logger from "@server/logger";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { getUserDeviceName } from "@server/db/names";
|
||||
|
||||
const getClientSchema = z.strictObject({
|
||||
clientId: z
|
||||
@@ -29,6 +30,7 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
|
||||
.from(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
|
||||
.limit(1);
|
||||
return res;
|
||||
} else if (niceId && orgId) {
|
||||
@@ -37,6 +39,7 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
|
||||
.from(clients)
|
||||
.where(and(eq(clients.niceId, niceId), eq(clients.orgId, orgId)))
|
||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
|
||||
.limit(1);
|
||||
return res;
|
||||
}
|
||||
@@ -105,8 +108,16 @@ export async function getClient(
|
||||
);
|
||||
}
|
||||
|
||||
// Replace name with device name if OLM exists
|
||||
let clientName = client.clients.name;
|
||||
if (client.olms) {
|
||||
const model = client.fingerprints?.deviceModel || null;
|
||||
clientName = getUserDeviceName(model, client.clients.name);
|
||||
}
|
||||
|
||||
const data: GetClientResponse = {
|
||||
...client.clients,
|
||||
name: clientName,
|
||||
olmId: client.olms ? client.olms.olmId : null
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
roleClients,
|
||||
sites,
|
||||
userClients,
|
||||
clientSitesAssociationsCache
|
||||
clientSitesAssociationsCache,
|
||||
fingerprints
|
||||
} from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -27,6 +28,7 @@ import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import NodeCache from "node-cache";
|
||||
import semver from "semver";
|
||||
import { getUserDeviceName } from "@server/db/names";
|
||||
|
||||
const olmVersionCache = new NodeCache({ stdTTL: 3600 });
|
||||
|
||||
@@ -137,14 +139,17 @@ function queryClients(
|
||||
userEmail: users.email,
|
||||
niceId: clients.niceId,
|
||||
agent: olms.agent,
|
||||
approvalState: clients.approvalState,
|
||||
olmArchived: olms.archived,
|
||||
archived: clients.archived,
|
||||
blocked: clients.blocked
|
||||
blocked: clients.blocked,
|
||||
deviceModel: fingerprints.deviceModel
|
||||
})
|
||||
.from(clients)
|
||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||
.leftJoin(users, eq(clients.userId, users.userId))
|
||||
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
|
||||
.where(and(...conditions));
|
||||
}
|
||||
|
||||
@@ -163,21 +168,22 @@ async function getSiteAssociations(clientIds: number[]) {
|
||||
.where(inArray(clientSitesAssociationsCache.clientId, clientIds));
|
||||
}
|
||||
|
||||
type OlmWithUpdateAvailable = Awaited<ReturnType<typeof queryClients>>[0] & {
|
||||
type ClientWithSites = Omit<
|
||||
Awaited<ReturnType<typeof queryClients>>[0],
|
||||
"deviceModel"
|
||||
> & {
|
||||
sites: Array<{
|
||||
siteId: number;
|
||||
siteName: string | null;
|
||||
siteNiceId: string | null;
|
||||
}>;
|
||||
olmUpdateAvailable?: boolean;
|
||||
};
|
||||
|
||||
type OlmWithUpdateAvailable = ClientWithSites;
|
||||
|
||||
export type ListClientsResponse = {
|
||||
clients: Array<
|
||||
Awaited<ReturnType<typeof queryClients>>[0] & {
|
||||
sites: Array<{
|
||||
siteId: number;
|
||||
siteName: string | null;
|
||||
siteNiceId: string | null;
|
||||
}>;
|
||||
olmUpdateAvailable?: boolean;
|
||||
}
|
||||
>;
|
||||
clients: Array<ClientWithSites>;
|
||||
pagination: { total: number; limit: number; offset: number };
|
||||
};
|
||||
|
||||
@@ -307,11 +313,17 @@ export async function listClients(
|
||||
>
|
||||
);
|
||||
|
||||
// Merge clients with their site associations
|
||||
const clientsWithSites = clientsList.map((client) => ({
|
||||
...client,
|
||||
sites: sitesByClient[client.clientId] || []
|
||||
}));
|
||||
// Merge clients with their site associations and replace name with device name
|
||||
const clientsWithSites = clientsList.map((client) => {
|
||||
const model = client.deviceModel || null;
|
||||
const newName = getUserDeviceName(model, client.name);
|
||||
const { deviceModel, ...clientWithoutDeviceModel } = client;
|
||||
return {
|
||||
...clientWithoutDeviceModel,
|
||||
name: newName,
|
||||
sites: sitesByClient[client.clientId] || []
|
||||
};
|
||||
});
|
||||
|
||||
const latestOlVersionPromise = getLatestOlmVersion();
|
||||
|
||||
@@ -350,7 +362,7 @@ export async function listClients(
|
||||
|
||||
return response<ListClientsResponse>(res, {
|
||||
data: {
|
||||
clients: clientsWithSites,
|
||||
clients: olmsWithUpdates,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
limit,
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function unblockClient(
|
||||
// Unblock the client
|
||||
await db
|
||||
.update(clients)
|
||||
.set({ blocked: false })
|
||||
.set({ blocked: false, approvalState: null })
|
||||
.where(eq(clients.clientId, clientId));
|
||||
|
||||
return response(res, {
|
||||
|
||||
@@ -586,6 +586,14 @@ authenticated.get(
|
||||
verifyUserHasAction(ActionsEnum.listRoles),
|
||||
role.listRoles
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/role/:roleId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateRole),
|
||||
logActionAudit(ActionsEnum.updateRole),
|
||||
role.updateRole
|
||||
);
|
||||
// authenticated.get(
|
||||
// "/role/:roleId",
|
||||
// verifyRoleAccess,
|
||||
@@ -861,6 +869,12 @@ authenticated.get(
|
||||
olm.getUserOlm
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/user/:userId/olm/recover",
|
||||
verifyIsLoggedInUser,
|
||||
olm.recoverOlmWithFingerprint
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/idp/oidc",
|
||||
verifyUserIsServerAdmin,
|
||||
@@ -1107,6 +1121,21 @@ authRouter.post(
|
||||
auth.login
|
||||
);
|
||||
authRouter.post("/logout", auth.logout);
|
||||
authRouter.post(
|
||||
"/lookup-user",
|
||||
rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 15,
|
||||
keyGenerator: (req) =>
|
||||
`lookupUser:${req.body.identifier || ipKeyGenerator(req.ip || "")}`,
|
||||
handler: (req, res, next) => {
|
||||
const message = `You can only lookup users ${15} times every ${15} minutes. Please try again later.`;
|
||||
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||
},
|
||||
store: createStore()
|
||||
}),
|
||||
auth.lookupUser
|
||||
);
|
||||
authRouter.post(
|
||||
"/newt/get-token",
|
||||
rateLimit({
|
||||
|
||||
@@ -24,7 +24,8 @@ const bodySchema = z.strictObject({
|
||||
emailPath: z.string().optional(),
|
||||
namePath: z.string().optional(),
|
||||
scopes: z.string().nonempty(),
|
||||
autoProvision: z.boolean().optional()
|
||||
autoProvision: z.boolean().optional(),
|
||||
tags: z.string().optional()
|
||||
});
|
||||
|
||||
export type CreateIdpResponse = {
|
||||
@@ -75,7 +76,8 @@ export async function createOidcIdp(
|
||||
emailPath,
|
||||
namePath,
|
||||
name,
|
||||
autoProvision
|
||||
autoProvision,
|
||||
tags
|
||||
} = parsedBody.data;
|
||||
|
||||
const key = config.getRawConfig().server.secret!;
|
||||
@@ -90,7 +92,8 @@ export async function createOidcIdp(
|
||||
.values({
|
||||
name,
|
||||
autoProvision,
|
||||
type: "oidc"
|
||||
type: "oidc",
|
||||
tags
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ async function query(limit: number, offset: number) {
|
||||
type: idp.type,
|
||||
variant: idpOidcConfig.variant,
|
||||
orgCount: sql<number>`count(${idpOrg.orgId})`,
|
||||
autoProvision: idp.autoProvision
|
||||
autoProvision: idp.autoProvision,
|
||||
tags: idp.tags
|
||||
})
|
||||
.from(idp)
|
||||
.leftJoin(idpOrg, sql`${idp.idpId} = ${idpOrg.idpId}`)
|
||||
|
||||
@@ -30,7 +30,8 @@ const bodySchema = z.strictObject({
|
||||
scopes: z.string().optional(),
|
||||
autoProvision: z.boolean().optional(),
|
||||
defaultRoleMapping: z.string().optional(),
|
||||
defaultOrgMapping: z.string().optional()
|
||||
defaultOrgMapping: z.string().optional(),
|
||||
tags: z.string().optional()
|
||||
});
|
||||
|
||||
export type UpdateIdpResponse = {
|
||||
@@ -94,7 +95,8 @@ export async function updateOidcIdp(
|
||||
name,
|
||||
autoProvision,
|
||||
defaultRoleMapping,
|
||||
defaultOrgMapping
|
||||
defaultOrgMapping,
|
||||
tags
|
||||
} = parsedBody.data;
|
||||
|
||||
// Check if IDP exists and is of type OIDC
|
||||
@@ -127,7 +129,8 @@ export async function updateOidcIdp(
|
||||
name,
|
||||
autoProvision,
|
||||
defaultRoleMapping,
|
||||
defaultOrgMapping
|
||||
defaultOrgMapping,
|
||||
tags
|
||||
};
|
||||
|
||||
// only update if at least one key is not undefined
|
||||
|
||||
@@ -467,6 +467,14 @@ authenticated.put(
|
||||
role.createRole
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/role/:roleId",
|
||||
verifyApiKeyOrgAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.updateRole),
|
||||
logActionAudit(ActionsEnum.updateRole),
|
||||
role.updateRole
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/roles",
|
||||
verifyApiKeyOrgAccess,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -9,3 +9,4 @@ export * from "./listUserOlms";
|
||||
export * from "./getUserOlm";
|
||||
export * from "./handleOlmServerPeerAddMessage";
|
||||
export * from "./handleOlmUnRelayMessage";
|
||||
export * from "./recoverOlmWithFingerprint";
|
||||
|
||||
@@ -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,
|
||||
|
||||
120
server/routers/olm/recoverOlmWithFingerprint.ts
Normal file
120
server/routers/olm/recoverOlmWithFingerprint.ts
Normal 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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import { fromError } from "zod-validation-error";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
||||
|
||||
const createRoleParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -17,7 +19,8 @@ const createRoleParamsSchema = z.strictObject({
|
||||
|
||||
const createRoleSchema = z.strictObject({
|
||||
name: z.string().min(1).max(255),
|
||||
description: z.string().optional()
|
||||
description: z.string().optional(),
|
||||
requireDeviceApproval: z.boolean().optional()
|
||||
});
|
||||
|
||||
export const defaultRoleAllowedActions: ActionsEnum[] = [
|
||||
@@ -97,6 +100,11 @@ export async function createRole(
|
||||
);
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
if (build === "oss" || !isLicensed) {
|
||||
roleData.requireDeviceApproval = undefined;
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
const newRole = await trx
|
||||
.insert(roles)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { roles, orgs } from "@server/db";
|
||||
import { db, orgs, roles } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import { sql, eq } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const listRolesParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -38,7 +36,8 @@ async function queryRoles(orgId: string, limit: number, offset: number) {
|
||||
isAdmin: roles.isAdmin,
|
||||
name: roles.name,
|
||||
description: roles.description,
|
||||
orgName: orgs.name
|
||||
orgName: orgs.name,
|
||||
requireDeviceApproval: roles.requireDeviceApproval
|
||||
})
|
||||
.from(roles)
|
||||
.leftJoin(orgs, eq(roles.orgId, orgs.orgId))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { db, orgs, type Role } from "@server/db";
|
||||
import { roles } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
@@ -8,20 +8,28 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { build } from "@server/build";
|
||||
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
|
||||
|
||||
const updateRoleParamsSchema = z.strictObject({
|
||||
orgId: z.string(),
|
||||
roleId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
const updateRoleBodySchema = z
|
||||
.strictObject({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
description: z.string().optional()
|
||||
description: z.string().optional(),
|
||||
requireDeviceApproval: z.boolean().optional()
|
||||
})
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
error: "At least one field must be provided for update"
|
||||
});
|
||||
|
||||
export type UpdateRoleBody = z.infer<typeof updateRoleBodySchema>;
|
||||
|
||||
export type UpdateRoleResponse = Role;
|
||||
|
||||
export async function updateRole(
|
||||
req: Request,
|
||||
res: Response,
|
||||
@@ -48,13 +56,14 @@ export async function updateRole(
|
||||
);
|
||||
}
|
||||
|
||||
const { roleId } = parsedParams.data;
|
||||
const { roleId, orgId } = parsedParams.data;
|
||||
const updateData = parsedBody.data;
|
||||
|
||||
const role = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(eq(roles.roleId, roleId))
|
||||
.innerJoin(orgs, eq(roles.orgId, orgs.orgId))
|
||||
.limit(1);
|
||||
|
||||
if (role.length === 0) {
|
||||
@@ -66,7 +75,7 @@ export async function updateRole(
|
||||
);
|
||||
}
|
||||
|
||||
if (role[0].isAdmin) {
|
||||
if (role[0].roles.isAdmin) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
@@ -75,6 +84,11 @@ export async function updateRole(
|
||||
);
|
||||
}
|
||||
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
if (build === "oss" || !isLicensed) {
|
||||
updateData.requireDeviceApproval = undefined;
|
||||
}
|
||||
|
||||
const updatedRole = await db
|
||||
.update(roles)
|
||||
.set(updateData)
|
||||
|
||||
Reference in New Issue
Block a user