mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-17 12:22:42 +00:00
Compare commits
12 Commits
1.16.2-s.8
...
1.16.2-s.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2db4c6246 | ||
|
|
c4839fee08 | ||
|
|
965b7026f0 | ||
|
|
e14e15fcbb | ||
|
|
4ca5acf158 | ||
|
|
ea41fcc566 | ||
|
|
5736c1d8ce | ||
|
|
d142366dd9 | ||
|
|
bab09dff95 | ||
|
|
23d3345ab9 | ||
|
|
09a64815d4 | ||
|
|
6d5f969798 |
@@ -515,6 +515,6 @@ authenticated.post(
|
|||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
verifyLimits,
|
verifyLimits,
|
||||||
verifyUserHasAction(ActionsEnum.signSshKey),
|
verifyUserHasAction(ActionsEnum.signSshKey),
|
||||||
logActionAudit(ActionsEnum.signSshKey),
|
// logActionAudit(ActionsEnum.signSshKey), // it is handled inside of the function below so we can include more metadata
|
||||||
ssh.signSshKey
|
ssh.signSshKey
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
|
actionAuditLog,
|
||||||
db,
|
db,
|
||||||
|
logsDb,
|
||||||
newts,
|
newts,
|
||||||
roles,
|
roles,
|
||||||
roundTripMessageTracker,
|
roundTripMessageTracker,
|
||||||
@@ -34,6 +36,7 @@ import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResourc
|
|||||||
import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA";
|
import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { sendToClient } from "#private/routers/ws";
|
import { sendToClient } from "#private/routers/ws";
|
||||||
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
const paramsSchema = z.strictObject({
|
||||||
orgId: z.string().nonempty()
|
orgId: z.string().nonempty()
|
||||||
@@ -446,6 +449,20 @@ export async function signSshKey(
|
|||||||
sshHost = resource.destination;
|
sshHost = resource.destination;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await logsDb.insert(actionAuditLog).values({
|
||||||
|
timestamp: Math.floor(Date.now() / 1000),
|
||||||
|
orgId: orgId,
|
||||||
|
actorType: "user",
|
||||||
|
actor: req.user?.username ?? "",
|
||||||
|
actorId: req.user?.userId ?? "",
|
||||||
|
action: ActionsEnum.signSshKey,
|
||||||
|
metadata: JSON.stringify({
|
||||||
|
resourceId: resource.siteResourceId,
|
||||||
|
resource: resource.name,
|
||||||
|
siteId: resource.siteId,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
return response<SignSshKeyResponse>(res, {
|
return response<SignSshKeyResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
certificate: cert.certificate,
|
certificate: cert.certificate,
|
||||||
|
|||||||
@@ -197,6 +197,12 @@ const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map();
|
|||||||
// Config version tracking map (local to this node, resets on server restart)
|
// Config version tracking map (local to this node, resets on server restart)
|
||||||
const clientConfigVersions: Map<string, number> = new Map();
|
const clientConfigVersions: Map<string, number> = new Map();
|
||||||
|
|
||||||
|
// Tracks the last Unix timestamp (seconds) at which a ping was flushed to the
|
||||||
|
// DB for a given siteId. Resets on server restart which is fine – the first
|
||||||
|
// ping after startup will always write, re-establishing the online state.
|
||||||
|
const lastPingDbWrite: Map<number, number> = new Map();
|
||||||
|
const PING_DB_WRITE_INTERVAL = 45; // seconds
|
||||||
|
|
||||||
// Recovery tracking
|
// Recovery tracking
|
||||||
let isRedisRecoveryInProgress = false;
|
let isRedisRecoveryInProgress = false;
|
||||||
|
|
||||||
@@ -855,12 +861,16 @@ const setupConnection = async (
|
|||||||
const newtClient = client as Newt;
|
const newtClient = client as Newt;
|
||||||
ws.on("ping", async () => {
|
ws.on("ping", async () => {
|
||||||
if (!newtClient.siteId) return;
|
if (!newtClient.siteId) return;
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const lastWrite = lastPingDbWrite.get(newtClient.siteId) ?? 0;
|
||||||
|
if (now - lastWrite < PING_DB_WRITE_INTERVAL) return;
|
||||||
|
lastPingDbWrite.set(newtClient.siteId, now);
|
||||||
try {
|
try {
|
||||||
await db
|
await db
|
||||||
.update(sites)
|
.update(sites)
|
||||||
.set({
|
.set({
|
||||||
online: true,
|
online: true,
|
||||||
lastPing: Math.floor(Date.now() / 1000)
|
lastPing: now
|
||||||
})
|
})
|
||||||
.where(eq(sites.siteId, newtClient.siteId));
|
.where(eq(sites.siteId, newtClient.siteId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
|||||||
.set({
|
.set({
|
||||||
megabytesOut: sql`COALESCE(${sites.megabytesOut}, 0) + ${bytesIn}`,
|
megabytesOut: sql`COALESCE(${sites.megabytesOut}, 0) + ${bytesIn}`,
|
||||||
megabytesIn: sql`COALESCE(${sites.megabytesIn}, 0) + ${bytesOut}`,
|
megabytesIn: sql`COALESCE(${sites.megabytesIn}, 0) + ${bytesOut}`,
|
||||||
lastBandwidthUpdate: currentTime
|
lastBandwidthUpdate: currentTime,
|
||||||
})
|
})
|
||||||
.where(eq(sites.pubKey, publicKey))
|
.where(eq(sites.pubKey, publicKey))
|
||||||
.returning({
|
.returning({
|
||||||
@@ -321,4 +321,4 @@ export const receiveBandwidth = async (
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -309,6 +309,14 @@ authenticated.post(
|
|||||||
siteResource.removeClientFromSiteResource
|
siteResource.removeClientFromSiteResource
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/client/:clientId/resources",
|
||||||
|
verifyLimits,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
||||||
|
logActionAudit(ActionsEnum.setResourceUsers),
|
||||||
|
siteResource.batchAddClientToSiteResources
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
"/org/:orgId/resource",
|
"/org/:orgId/resource",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
// Prepare an array to store site configurations
|
// Prepare an array to store site configurations
|
||||||
logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`);
|
logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`);
|
||||||
|
|
||||||
let jitMode = true;
|
let jitMode = false;
|
||||||
if (sitesCount > 250 && build == "saas") {
|
if (sitesCount > 250 && build == "saas") {
|
||||||
// THIS IS THE MAX ON THE BUSINESS TIER
|
// THIS IS THE MAX ON THE BUSINESS TIER
|
||||||
// we have too many sites
|
// we have too many sites
|
||||||
|
|||||||
247
server/routers/siteResource/batchAddClientToSiteResources.ts
Normal file
247
server/routers/siteResource/batchAddClientToSiteResources.ts
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
clients,
|
||||||
|
clientSiteResources,
|
||||||
|
siteResources,
|
||||||
|
apiKeyOrg
|
||||||
|
} 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, inArray } from "drizzle-orm";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import {
|
||||||
|
rebuildClientAssociationsFromClient,
|
||||||
|
rebuildClientAssociationsFromSiteResource
|
||||||
|
} from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
|
const batchAddClientToSiteResourcesParamsSchema = z
|
||||||
|
.object({
|
||||||
|
clientId: z.string().transform(Number).pipe(z.number().int().positive())
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const batchAddClientToSiteResourcesBodySchema = z
|
||||||
|
.object({
|
||||||
|
siteResourceIds: z
|
||||||
|
.array(z.number().int().positive())
|
||||||
|
.min(1, "At least one siteResourceId is required")
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "post",
|
||||||
|
path: "/client/{clientId}/resources",
|
||||||
|
description: "Add a machine client to multiple site resources at once.",
|
||||||
|
tags: [OpenAPITags.PrivateResource, OpenAPITags.Client],
|
||||||
|
request: {
|
||||||
|
params: batchAddClientToSiteResourcesParamsSchema,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: batchAddClientToSiteResourcesBodySchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function batchAddClientToSiteResources(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedParams =
|
||||||
|
batchAddClientToSiteResourcesParamsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedBody = batchAddClientToSiteResourcesBodySchema.safeParse(
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { clientId } = parsedParams.data;
|
||||||
|
const { siteResourceIds } = parsedBody.data;
|
||||||
|
const uniqueSiteResourceIds = [...new Set(siteResourceIds)];
|
||||||
|
|
||||||
|
const batchSiteResources = await db
|
||||||
|
.select()
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
inArray(siteResources.siteResourceId, uniqueSiteResourceIds)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (batchSiteResources.length !== uniqueSiteResourceIds.length) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"One or more site resources not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiKey.isRoot) {
|
||||||
|
const orgIds = [
|
||||||
|
...new Set(batchSiteResources.map((sr) => sr.orgId))
|
||||||
|
];
|
||||||
|
if (orgIds.length > 1) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"All site resources must belong to the same organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const orgId = orgIds[0];
|
||||||
|
const [apiKeyOrgRow] = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeyOrg)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
|
eq(apiKeyOrg.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!apiKeyOrgRow) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to the organization of the specified site resources"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [clientInOrg] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clients.clientId, clientId),
|
||||||
|
eq(clients.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!clientInOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to the specified client"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, "Client not found")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.userId !== null) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"This endpoint only supports machine (non-user) clients; the specified client is associated with a user"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingEntries = await db
|
||||||
|
.select({
|
||||||
|
siteResourceId: clientSiteResources.siteResourceId
|
||||||
|
})
|
||||||
|
.from(clientSiteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clientSiteResources.clientId, clientId),
|
||||||
|
inArray(
|
||||||
|
clientSiteResources.siteResourceId,
|
||||||
|
batchSiteResources.map((sr) => sr.siteResourceId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const existingSiteResourceIds = new Set(
|
||||||
|
existingEntries.map((e) => e.siteResourceId)
|
||||||
|
);
|
||||||
|
const siteResourcesToAdd = batchSiteResources.filter(
|
||||||
|
(sr) => !existingSiteResourceIds.has(sr.siteResourceId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (siteResourcesToAdd.length === 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.CONFLICT,
|
||||||
|
"Client is already assigned to all specified site resources"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
for (const siteResource of siteResourcesToAdd) {
|
||||||
|
await trx.insert(clientSiteResources).values({
|
||||||
|
clientId,
|
||||||
|
siteResourceId: siteResource.siteResourceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await rebuildClientAssociationsFromClient(client, trx);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: {
|
||||||
|
addedCount: siteResourcesToAdd.length,
|
||||||
|
skippedCount:
|
||||||
|
batchSiteResources.length - siteResourcesToAdd.length,
|
||||||
|
siteResourceIds: siteResourcesToAdd.map(
|
||||||
|
(sr) => sr.siteResourceId
|
||||||
|
)
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: `Client added to ${siteResourcesToAdd.length} site resource(s) successfully`,
|
||||||
|
status: HttpCode.CREATED
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,4 +15,5 @@ export * from "./addUserToSiteResource";
|
|||||||
export * from "./removeUserFromSiteResource";
|
export * from "./removeUserFromSiteResource";
|
||||||
export * from "./setSiteResourceClients";
|
export * from "./setSiteResourceClients";
|
||||||
export * from "./addClientToSiteResource";
|
export * from "./addClientToSiteResource";
|
||||||
|
export * from "./batchAddClientToSiteResources";
|
||||||
export * from "./removeClientFromSiteResource";
|
export * from "./removeClientFromSiteResource";
|
||||||
|
|||||||
Reference in New Issue
Block a user