mirror of
https://github.com/fosrl/pangolin.git
synced 2025-12-17 21:45:51 +00:00
add basic org policy check in middleware
This commit is contained in:
@@ -25,7 +25,8 @@ export const orgs = pgTable("orgs", {
|
|||||||
orgId: varchar("orgId").primaryKey(),
|
orgId: varchar("orgId").primaryKey(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
subnet: varchar("subnet"),
|
subnet: varchar("subnet"),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt"),
|
||||||
|
requireTwoFactor: boolean("requireTwoFactor").default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgDomains = pgTable("orgDomains", {
|
export const orgDomains = pgTable("orgDomains", {
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export const orgs = sqliteTable("orgs", {
|
|||||||
orgId: text("orgId").primaryKey(),
|
orgId: text("orgId").primaryKey(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
subnet: text("subnet"),
|
subnet: text("subnet"),
|
||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt"),
|
||||||
|
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userDomains = sqliteTable("userDomains", {
|
export const userDomains = sqliteTable("userDomains", {
|
||||||
@@ -141,11 +142,15 @@ export const targets = sqliteTable("targets", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||||
targetHealthCheckId: integer("targetHealthCheckId").primaryKey({ autoIncrement: true }),
|
targetHealthCheckId: integer("targetHealthCheckId").primaryKey({
|
||||||
|
autoIncrement: true
|
||||||
|
}),
|
||||||
targetId: integer("targetId")
|
targetId: integer("targetId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => targets.targetId, { onDelete: "cascade" }),
|
.references(() => targets.targetId, { onDelete: "cascade" }),
|
||||||
hcEnabled: integer("hcEnabled", { mode: "boolean" }).notNull().default(false),
|
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
hcPath: text("hcPath"),
|
hcPath: text("hcPath"),
|
||||||
hcScheme: text("hcScheme"),
|
hcScheme: text("hcScheme"),
|
||||||
hcMode: text("hcMode").default("http"),
|
hcMode: text("hcMode").default("http"),
|
||||||
@@ -155,7 +160,9 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
|||||||
hcUnhealthyInterval: integer("hcUnhealthyInterval").default(30), // in seconds
|
hcUnhealthyInterval: integer("hcUnhealthyInterval").default(30), // in seconds
|
||||||
hcTimeout: integer("hcTimeout").default(5), // in seconds
|
hcTimeout: integer("hcTimeout").default(5), // in seconds
|
||||||
hcHeaders: text("hcHeaders"),
|
hcHeaders: text("hcHeaders"),
|
||||||
hcFollowRedirects: integer("hcFollowRedirects", { mode: "boolean" }).default(true),
|
hcFollowRedirects: integer("hcFollowRedirects", {
|
||||||
|
mode: "boolean"
|
||||||
|
}).default(true),
|
||||||
hcMethod: text("hcMethod").default("GET"),
|
hcMethod: text("hcMethod").default("GET"),
|
||||||
hcStatus: integer("hcStatus"), // http code
|
hcStatus: integer("hcStatus"), // http code
|
||||||
hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy"
|
hcHealth: text("hcHealth").default("unknown") // "unknown", "healthy", "unhealthy"
|
||||||
|
|||||||
17
server/lib/checkOrgAccessPolicy.ts
Normal file
17
server/lib/checkOrgAccessPolicy.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Org, User } from "@server/db";
|
||||||
|
|
||||||
|
type CheckOrgAccessPolicyProps = {
|
||||||
|
orgId?: string;
|
||||||
|
org?: Org;
|
||||||
|
userId?: string;
|
||||||
|
user?: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function checkOrgAccessPolicy(
|
||||||
|
props: CheckOrgAccessPolicyProps
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
@@ -40,6 +40,10 @@ export class License {
|
|||||||
public setServerSecret(secret: string) {
|
public setServerSecret(secret: string) {
|
||||||
this.serverSecret = secret;
|
this.serverSecret = secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async isUnlocked() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await setHostMeta();
|
await setHostMeta();
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { db } from "@server/db";
|
import { db, orgs } from "@server/db";
|
||||||
import { userOrgs } from "@server/db";
|
import { userOrgs } from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
export async function verifyOrgAccess(
|
export async function verifyOrgAccess(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -43,12 +44,27 @@ export async function verifyOrgAccess(
|
|||||||
"User does not have access to this organization"
|
"User does not have access to this organization"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!policyCheck.success || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Failed organization access policy check: " +
|
||||||
|
(policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// User has access, attach the user's role to the request for potential future use
|
// User has access, attach the user's role to the request for potential future use
|
||||||
req.userOrgRoleId = req.userOrg.roleId;
|
req.userOrgRoleId = req.userOrg.roleId;
|
||||||
req.userOrgId = orgId;
|
req.userOrgId = orgId;
|
||||||
return next();
|
return next();
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|||||||
95
server/private/lib/checkOrgAccessPolicy.ts
Normal file
95
server/private/lib/checkOrgAccessPolicy.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import { db, Org, orgs, User, users } from "@server/db";
|
||||||
|
import { getOrgTierData } from "#private/lib/billing";
|
||||||
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
|
import license from "#private/license/license";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
type CheckOrgAccessPolicyProps = {
|
||||||
|
orgId?: string;
|
||||||
|
org?: Org;
|
||||||
|
userId?: string;
|
||||||
|
user?: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function checkOrgAccessPolicy(
|
||||||
|
props: CheckOrgAccessPolicyProps
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}> {
|
||||||
|
const userId = props.userId || props.user?.userId;
|
||||||
|
const orgId = props.orgId || props.org?.orgId;
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
return { success: false, error: "Organization ID is required" };
|
||||||
|
}
|
||||||
|
if (!userId) {
|
||||||
|
return { success: false, error: "User ID is required" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (build === "saas") {
|
||||||
|
const { tier } = await getOrgTierData(orgId);
|
||||||
|
const subscribed = tier === TierId.STANDARD;
|
||||||
|
// if not subscribed, don't check the policies
|
||||||
|
if (!subscribed) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (build === "enterprise") {
|
||||||
|
const isUnlocked = await license.isUnlocked();
|
||||||
|
// if not licensed, don't check the policies
|
||||||
|
if (!isUnlocked) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the needed data
|
||||||
|
|
||||||
|
if (!props.org) {
|
||||||
|
const [orgQuery] = await db
|
||||||
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, orgId));
|
||||||
|
props.org = orgQuery;
|
||||||
|
if (!props.org) {
|
||||||
|
return { success: false, error: "Organization not found" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.user) {
|
||||||
|
const [userQuery] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.userId, userId));
|
||||||
|
props.user = userQuery;
|
||||||
|
if (!props.user) {
|
||||||
|
return { success: false, error: "User not found" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now check the policies
|
||||||
|
|
||||||
|
if (!props.org.requireTwoFactor && !props.user.twoFactorEnabled) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Two-factor authentication is required"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user