mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-17 04:12:45 +00:00
Compare commits
1 Commits
dev
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10349932f4 |
2
.github/workflows/cicd.yml
vendored
2
.github/workflows/cicd.yml
vendored
@@ -415,7 +415,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
# cosign is used to sign and verify container images (key and keyless)
|
# cosign is used to sign and verify container images (key and keyless)
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
||||||
|
|
||||||
- name: Dual-sign and verify (GHCR & Docker Hub)
|
- name: Dual-sign and verify (GHCR & Docker Hub)
|
||||||
# Sign each image by digest using keyless (OIDC) and key-based signing,
|
# Sign each image by digest using keyless (OIDC) and key-based signing,
|
||||||
|
|||||||
2
.github/workflows/mirror.yaml
vendored
2
.github/workflows/mirror.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
skopeo --version
|
skopeo --version
|
||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
||||||
|
|
||||||
- name: Input check
|
- name: Input check
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -515,6 +515,6 @@ authenticated.post(
|
|||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
verifyLimits,
|
verifyLimits,
|
||||||
verifyUserHasAction(ActionsEnum.signSshKey),
|
verifyUserHasAction(ActionsEnum.signSshKey),
|
||||||
// logActionAudit(ActionsEnum.signSshKey), // it is handled inside of the function below so we can include more metadata
|
logActionAudit(ActionsEnum.signSshKey),
|
||||||
ssh.signSshKey
|
ssh.signSshKey
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,9 +14,7 @@
|
|||||||
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,
|
||||||
@@ -36,7 +34,6 @@ 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()
|
||||||
@@ -449,20 +446,6 @@ 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,12 +197,6 @@ 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;
|
||||||
|
|
||||||
@@ -861,16 +855,12 @@ 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: now
|
lastPing: Math.floor(Date.now() / 1000)
|
||||||
})
|
})
|
||||||
.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({
|
||||||
|
|||||||
@@ -309,14 +309,6 @@ authenticated.post(
|
|||||||
siteResource.removeClientFromSiteResource
|
siteResource.removeClientFromSiteResource
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/client/:clientId/site-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 = false;
|
let jitMode = true;
|
||||||
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
|
||||||
|
|||||||
@@ -1,247 +0,0 @@
|
|||||||
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}/site-resources",
|
|
||||||
description: "Add a machine client to multiple site resources at once.",
|
|
||||||
tags: [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,5 +15,4 @@ 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