diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index fdd6bfa0..e49ed352 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -225,8 +225,8 @@ export const siteResources = pgTable("siteResources", { enabled: boolean("enabled").notNull().default(true), alias: varchar("alias"), aliasAddress: varchar("aliasAddress"), - tcpPortRangeString: varchar("tcpPortRangeString"), - udpPortRangeString: varchar("udpPortRangeString"), + tcpPortRangeString: varchar("tcpPortRangeString").notNull().default("*"), + udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"), disableIcmp: boolean("disableIcmp").notNull().default(false) }); diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 0f29bab1..5f60b23e 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -253,9 +253,9 @@ export const siteResources = sqliteTable("siteResources", { enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), alias: text("alias"), aliasAddress: text("aliasAddress"), - tcpPortRangeString: text("tcpPortRangeString"), - udpPortRangeString: text("udpPortRangeString"), - disableIcmp: integer("disableIcmp", { mode: "boolean" }) + tcpPortRangeString: text("tcpPortRangeString").notNull().default("*"), + udpPortRangeString: text("udpPortRangeString").notNull().default("*"), + disableIcmp: integer("disableIcmp", { mode: "boolean" }).notNull().default(false) }); export const clientSiteResources = sqliteTable("clientSiteResources", { diff --git a/server/private/lib/checkOrgAccessPolicy.ts b/server/private/lib/checkOrgAccessPolicy.ts index 7a78803d..cb40c8b8 100644 --- a/server/private/lib/checkOrgAccessPolicy.ts +++ b/server/private/lib/checkOrgAccessPolicy.ts @@ -23,10 +23,10 @@ import { } from "@server/lib/checkOrgAccessPolicy"; import { UserType } from "@server/types/UserTypes"; -export async function enforceResourceSessionLength( +export function enforceResourceSessionLength( resourceSession: ResourceSession, org: Org -): Promise<{ valid: boolean; error?: string }> { +): { valid: boolean; error?: string } { if (org.maxSessionLengthHours) { const sessionIssuedAt = resourceSession.issuedAt; // may be null const maxSessionLengthHours = org.maxSessionLengthHours; diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index bb3fd2c5..62c60696 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -71,7 +71,8 @@ export async function getTraefikConfig( siteTypes: string[], filterOutNamespaceDomains = false, generateLoginPageRouters = false, - allowRawResources = true + allowRawResources = true, + allowMaintenancePage = true ): Promise { // Get resources with their targets and sites in a single optimized query // Start from sites on this exit node, then join to targets and resources diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index 14ed1b8b..009b2fe1 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -39,7 +39,8 @@ import { resourceHeaderAuthExtendedCompatibility, ResourceHeaderAuthExtendedCompatibility, orgs, - requestAuditLog + requestAuditLog, + Org } from "@server/db"; import { resources, @@ -79,6 +80,7 @@ import { maxmindLookup } from "@server/db/maxmind"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; import semver from "semver"; import { maxmindAsnLookup } from "@server/db/maxmindAsn"; +import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy"; // Zod schemas for request validation const getResourceByDomainParamsSchema = z.strictObject({ @@ -94,6 +96,12 @@ const getUserOrgRoleParamsSchema = z.strictObject({ orgId: z.string().min(1, "Organization ID is required") }); +const getUserOrgSessionVerifySchema = z.strictObject({ + userId: z.string().min(1, "User ID is required"), + orgId: z.string().min(1, "Organization ID is required"), + sessionId: z.string().min(1, "Session ID is required") +}); + const getRoleResourceAccessParamsSchema = z.strictObject({ roleId: z .string() @@ -178,6 +186,7 @@ export type ResourceWithAuth = { password: ResourcePassword | null; headerAuth: ResourceHeaderAuth | null; headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; + org: Org }; export type UserSessionWithUser = { @@ -508,6 +517,7 @@ hybridRouter.get( resources.resourceId ) ) + .innerJoin(orgs, eq(orgs.orgId, resources.orgId)) .where(eq(resources.fullDomain, domain)) .limit(1); @@ -542,7 +552,8 @@ hybridRouter.get( password: result.resourcePassword, headerAuth: result.resourceHeaderAuth, headerAuthExtendedCompatibility: - result.resourceHeaderAuthExtendedCompatibility + result.resourceHeaderAuthExtendedCompatibility, + org: result.orgs }; return response(res, { @@ -822,6 +833,69 @@ hybridRouter.get( } ); +// Get user organization role +hybridRouter.get( + "/user/:userId/org/:orgId/session/:sessionId/verify", + async (req: Request, res: Response, next: NextFunction) => { + try { + const parsedParams = getUserOrgSessionVerifySchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { userId, orgId, sessionId } = parsedParams.data; + const remoteExitNode = req.remoteExitNode; + + if (!remoteExitNode || !remoteExitNode.exitNodeId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Remote exit node not found" + ) + ); + } + + if (await checkExitNodeOrg(remoteExitNode.exitNodeId, orgId)) { + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + "User is not authorized to access this organization" + ) + ); + } + + const accessPolicy = await checkOrgAccessPolicy({ + orgId, + userId, + sessionId + }); + + return response(res, { + data: accessPolicy, + success: true, + error: false, + message: "User org access policy retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to get user org role" + ) + ); + } + } +); + // Check if role has access to resource hybridRouter.get( "/role/:roleId/resource/:resourceId/access", diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index 3a1140b3..bf3239d0 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -911,7 +911,7 @@ WantedBy=default.target` ? "squareOutlinePrimary" : "squareOutline" } - className={`flex-1 min-w-[120px] ${platform === os ? "bg-primary/10" : ""} shadow-none`} + className={`flex-1 min-w-30 ${platform === os ? "bg-primary/10" : ""} shadow-none`} onClick={() => { setPlatform(os); }} @@ -942,7 +942,7 @@ WantedBy=default.target` ? "squareOutlinePrimary" : "squareOutline" } - className={`flex-1 min-w-[120px] ${architecture === arch ? "bg-primary/10" : ""} shadow-none`} + className={`flex-1 min-w-30 ${architecture === arch ? "bg-primary/10" : ""} shadow-none`} onClick={() => setArchitecture( arch diff --git a/src/components/ProductUpdates.tsx b/src/components/ProductUpdates.tsx index f8c4bf11..2410d8b4 100644 --- a/src/components/ProductUpdates.tsx +++ b/src/components/ProductUpdates.tsx @@ -192,7 +192,7 @@ function ProductUpdatesListPopup({