diff --git a/server/lib/blueprints/applyNewtDockerBlueprint.ts b/server/lib/blueprints/applyNewtDockerBlueprint.ts index 93794461c..6b91b3462 100644 --- a/server/lib/blueprints/applyNewtDockerBlueprint.ts +++ b/server/lib/blueprints/applyNewtDockerBlueprint.ts @@ -71,7 +71,10 @@ export async function applyNewtDockerBlueprint( let skippedKeys: string[] = []; try { - const blueprint = processContainerLabels(containers); + // Some Newt clients can report null/undefined containers when Docker + // labels are unavailable. Treat that as an empty blueprint payload. + const safeContainers = Array.isArray(containers) ? containers : []; + const blueprint = processContainerLabels(safeContainers); logger.debug( `Received Docker blueprint with ${Object.keys(blueprint["proxy-resources"]).length} proxy, ${Object.keys(blueprint["client-resources"]).length} client resource(s)` diff --git a/server/lib/blueprints/publicResources.ts b/server/lib/blueprints/publicResources.ts index b60970310..1bbe0a4f1 100644 --- a/server/lib/blueprints/publicResources.ts +++ b/server/lib/blueprints/publicResources.ts @@ -945,7 +945,45 @@ export async function updatePublicResources( } } else { // INLINE POLICY MODE: sync rules into policy-level table - const inlinePolicyId = resource!.defaultResourcePolicyId!; + let inlinePolicyId = resource!.defaultResourcePolicyId; + + // Targets-only updates skip the auth/policy update branch above, + // so pre-1.19 resources can still have no inline policy linked. + if (!inlinePolicyId) { + const [adminRole] = await trx + .select() + .from(roles) + .where( + and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)) + ) + .limit(1); + + if (!adminRole) { + throw new Error(`Admin role not found`); + } + + inlinePolicyId = await ensureInlinePolicy( + existingResource.defaultResourcePolicyId, + orgId, + resourceNiceId, + adminRole.roleId, + trx + ); + + [resource] = await trx + .update(resources) + .set({ + resourcePolicyId: null, + defaultResourcePolicyId: inlinePolicyId + }) + .where( + eq( + resources.resourceId, + existingResource.resourceId + ) + ) + .returning(); + } // Clear the old resource-level rules table await trx diff --git a/src/app/ssh/page.tsx b/src/app/ssh/page.tsx index aa1758f7e..ad65bd804 100644 --- a/src/app/ssh/page.tsx +++ b/src/app/ssh/page.tsx @@ -3,7 +3,7 @@ import { priv } from "@app/lib/api"; import { generateBrowserGatewayMetadata } from "@app/lib/browserGatewayMetadata"; import { getBrowserTargetForRequest } from "@app/lib/getBrowserTargetForRequest"; import { loadOrgLoginPageBranding } from "@app/lib/loadOrgLoginPageBranding"; -import { AxiosResponse } from "axios"; +import axios, { AxiosResponse } from "axios"; import { GetBrowserTargetResponse } from "@server/routers/browserGatewayTarget"; import SshClient from "./SshClient"; import crypto from "crypto"; @@ -152,8 +152,12 @@ export default async function SshPage() { await waitForRoundTripCompletion(messageIds, cookieHeader); } catch (err) { console.error("Error signing SSH key:", err); - const detail = err instanceof Error ? err.message : String(err); - error = `${t("sshErrorSignKeyFailed")}: ${detail}`; + if (axios.isAxiosError(err) && err.response?.status === 403) { + error = t("accessDeniedDescription"); + } else { + const detail = err instanceof Error ? err.message : String(err); + error = `${t("sshErrorSignKeyFailed")}: ${detail}`; + } } }