mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
enforce resource session length
This commit is contained in:
@@ -50,7 +50,8 @@ export async function createResourceSession(opts: {
|
||||
doNotExtend: opts.doNotExtend || false,
|
||||
accessTokenId: opts.accessTokenId || null,
|
||||
isRequestToken: opts.isRequestToken || false,
|
||||
userSessionId: opts.userSessionId || null
|
||||
userSessionId: opts.userSessionId || null,
|
||||
issuedAt: new Date().getTime()
|
||||
};
|
||||
|
||||
await db.insert(resourceSessions).values(session);
|
||||
|
||||
@@ -447,7 +447,8 @@ export const resourceSessions = pgTable("resourceSessions", {
|
||||
{
|
||||
onDelete: "cascade"
|
||||
}
|
||||
)
|
||||
),
|
||||
issuedAt: bigint("issuedAt", { mode: "number" })
|
||||
});
|
||||
|
||||
export const resourceWhitelist = pgTable("resourceWhitelist", {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db, loginPage, LoginPage, loginPageOrg } from "@server/db";
|
||||
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs } from "@server/db";
|
||||
import {
|
||||
Resource,
|
||||
ResourcePassword,
|
||||
@@ -23,6 +23,7 @@ export type ResourceWithAuth = {
|
||||
pincode: ResourcePincode | null;
|
||||
password: ResourcePassword | null;
|
||||
headerAuth: ResourceHeaderAuth | null;
|
||||
org: Org;
|
||||
};
|
||||
|
||||
export type UserSessionWithUser = {
|
||||
@@ -51,6 +52,10 @@ export async function getResourceByDomain(
|
||||
resourceHeaderAuth,
|
||||
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
||||
)
|
||||
.innerJoin(
|
||||
orgs,
|
||||
eq(orgs.orgId, resources.orgId)
|
||||
)
|
||||
.where(eq(resources.fullDomain, domain))
|
||||
.limit(1);
|
||||
|
||||
@@ -62,7 +67,8 @@ export async function getResourceByDomain(
|
||||
resource: result.resources,
|
||||
pincode: result.resourcePincode,
|
||||
password: result.resourcePassword,
|
||||
headerAuth: result.resourceHeaderAuth
|
||||
headerAuth: result.resourceHeaderAuth,
|
||||
org: result.orgs
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -587,7 +587,8 @@ export const resourceSessions = sqliteTable("resourceSessions", {
|
||||
{
|
||||
onDelete: "cascade"
|
||||
}
|
||||
)
|
||||
),
|
||||
issuedAt: integer("issuedAt")
|
||||
});
|
||||
|
||||
export const resourceWhitelist = sqliteTable("resourceWhitelist", {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Org, Session, User } from "@server/db";
|
||||
import { Org, ResourceSession, Session, User } from "@server/db";
|
||||
|
||||
export type CheckOrgAccessPolicyProps = {
|
||||
orgId?: string;
|
||||
@@ -27,6 +27,13 @@ export type CheckOrgAccessPolicyResult = {
|
||||
};
|
||||
};
|
||||
|
||||
export async function enforceResourceSessionLength(
|
||||
resourceSession: ResourceSession,
|
||||
org: Org
|
||||
): Promise<{ valid: boolean; error?: string }> {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
export async function checkOrgAccessPolicy(
|
||||
props: CheckOrgAccessPolicyProps
|
||||
): Promise<{
|
||||
|
||||
@@ -12,7 +12,14 @@
|
||||
*/
|
||||
|
||||
import { build } from "@server/build";
|
||||
import { db, Org, orgs, sessions, User, users } from "@server/db";
|
||||
import {
|
||||
db,
|
||||
Org,
|
||||
orgs,
|
||||
ResourceSession,
|
||||
sessions,
|
||||
users
|
||||
} from "@server/db";
|
||||
import { getOrgTierData } from "#private/lib/billing";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
import license from "#private/license/license";
|
||||
@@ -23,6 +30,35 @@ import {
|
||||
} from "@server/lib/checkOrgAccessPolicy";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
|
||||
export async function enforceResourceSessionLength(
|
||||
resourceSession: ResourceSession,
|
||||
org: Org
|
||||
): Promise<{ valid: boolean; error?: string }> {
|
||||
if (org.maxSessionLengthHours) {
|
||||
const sessionIssuedAt = resourceSession.issuedAt; // may be null
|
||||
const maxSessionLengthHours = org.maxSessionLengthHours;
|
||||
|
||||
if (sessionIssuedAt) {
|
||||
const maxSessionLengthMs = maxSessionLengthHours * 60 * 60 * 1000;
|
||||
const sessionAgeMs = Date.now() - sessionIssuedAt;
|
||||
|
||||
if (sessionAgeMs > maxSessionLengthMs) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Resource session has expired due to organization policy (max session length: ${maxSessionLengthHours} hours)`
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Resource session is invalid due to organization policy (max session length: ${maxSessionLengthHours} hours)`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
export async function checkOrgAccessPolicy(
|
||||
props: CheckOrgAccessPolicyProps
|
||||
): Promise<CheckOrgAccessPolicyResult> {
|
||||
@@ -43,15 +79,6 @@ export async function checkOrgAccessPolicy(
|
||||
return { allowed: false, error: "Session 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 { allowed: true };
|
||||
}
|
||||
}
|
||||
|
||||
if (build === "enterprise") {
|
||||
const isUnlocked = await license.isUnlocked();
|
||||
// if not licensed, don't check the policies
|
||||
|
||||
@@ -73,8 +73,6 @@ import { validateResourceSessionToken } from "@server/auth/sessions/resource";
|
||||
import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
|
||||
import { maxmindLookup } from "@server/db/maxmind";
|
||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||
import { CheckOrgAccessPolicyResult } from "@server/lib/checkOrgAccessPolicy";
|
||||
|
||||
// Zod schemas for request validation
|
||||
const getResourceByDomainParamsSchema = z
|
||||
@@ -1585,90 +1583,3 @@ hybridRouter.post(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const getOrgAccessPolicyParamsSchema = z
|
||||
.object({
|
||||
orgId: z.string().min(1),
|
||||
userId: z.string().min(1)
|
||||
})
|
||||
.strict();
|
||||
|
||||
const getOrgAccessPolicyBodySchema = z
|
||||
.object({
|
||||
sessionId: z.string().min(1)
|
||||
})
|
||||
.strict();
|
||||
|
||||
hybridRouter.get(
|
||||
"/org/:orgId/user/:userId/access",
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const parsedParams = getOrgAccessPolicyParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedBody = getOrgAccessPolicyBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, userId } = parsedParams.data;
|
||||
const { sessionId } = parsedBody.data;
|
||||
const remoteExitNode = req.remoteExitNode;
|
||||
|
||||
if (!remoteExitNode?.exitNodeId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Remote exit node not found"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (await checkExitNodeOrg(remoteExitNode.exitNodeId, orgId)) {
|
||||
// If the exit node is not allowed for the org, return an error
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Exit node not allowed for this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const accessPolicy = await checkOrgAccessPolicy({
|
||||
orgId,
|
||||
userId,
|
||||
sessionId
|
||||
});
|
||||
|
||||
return response<CheckOrgAccessPolicyResult>(res, {
|
||||
data: accessPolicy,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Org access policy retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to get org login page"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -16,12 +16,14 @@ import {
|
||||
} from "@server/db/queries/verifySessionQueries";
|
||||
import {
|
||||
LoginPage,
|
||||
Org,
|
||||
Resource,
|
||||
ResourceAccessToken,
|
||||
ResourceHeaderAuth,
|
||||
ResourcePassword,
|
||||
ResourcePincode,
|
||||
ResourceRule
|
||||
ResourceRule,
|
||||
resourceSessions
|
||||
} from "@server/db";
|
||||
import config from "@server/lib/config";
|
||||
import { isIpInCidr } from "@server/lib/ip";
|
||||
@@ -37,7 +39,10 @@ import { getCountryCodeForIp } from "@server/lib/geoip";
|
||||
import { getOrgTierData } from "#dynamic/lib/billing";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
import { verifyPassword } from "@server/auth/password";
|
||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||
import {
|
||||
checkOrgAccessPolicy,
|
||||
enforceResourceSessionLength
|
||||
} from "#dynamic/lib/checkOrgAccessPolicy";
|
||||
|
||||
// We'll see if this speeds anything up
|
||||
const cache = new NodeCache({
|
||||
@@ -142,6 +147,7 @@ export async function verifyResourceSession(
|
||||
pincode: ResourcePincode | null;
|
||||
password: ResourcePassword | null;
|
||||
headerAuth: ResourceHeaderAuth | null;
|
||||
org: Org;
|
||||
}
|
||||
| undefined = cache.get(resourceCacheKey);
|
||||
|
||||
@@ -380,6 +386,22 @@ export async function verifyResourceSession(
|
||||
}
|
||||
|
||||
if (resourceSession) {
|
||||
// only run this check if not SSO sesion; SSO session length is checked later
|
||||
if (!(resourceSessions.userSessionId && sso)) {
|
||||
const accessPolicy = await enforceResourceSessionLength(
|
||||
resourceSession,
|
||||
resourceData.org
|
||||
);
|
||||
|
||||
if (!accessPolicy.valid) {
|
||||
logger.debug(
|
||||
"Resource session invalid due to org policy:",
|
||||
accessPolicy.error
|
||||
);
|
||||
return notAllowed(res, redirectPath, resource.orgId);
|
||||
}
|
||||
}
|
||||
|
||||
if (pincode && resourceSession.pincodeId) {
|
||||
logger.debug(
|
||||
"Resource allowed because pincode session is valid"
|
||||
@@ -422,7 +444,8 @@ export async function verifyResourceSession(
|
||||
if (allowedUserData === undefined) {
|
||||
allowedUserData = await isUserAllowedToAccessResource(
|
||||
resourceSession.userSessionId,
|
||||
resource
|
||||
resource,
|
||||
resourceData.org
|
||||
);
|
||||
|
||||
cache.set(userAccessCacheKey, allowedUserData);
|
||||
@@ -564,7 +587,8 @@ function allowed(res: Response, userData?: BasicUserData) {
|
||||
|
||||
async function isUserAllowedToAccessResource(
|
||||
userSessionId: string,
|
||||
resource: Resource
|
||||
resource: Resource,
|
||||
org: Org
|
||||
): Promise<BasicUserData | null> {
|
||||
const result = await getUserSessionWithUser(userSessionId);
|
||||
|
||||
@@ -592,9 +616,9 @@ async function isUserAllowedToAccessResource(
|
||||
}
|
||||
|
||||
const accessPolicy = await checkOrgAccessPolicy({
|
||||
orgId: resource.orgId,
|
||||
userId: user.userId,
|
||||
sessionId: session.sessionId
|
||||
org,
|
||||
user,
|
||||
session
|
||||
});
|
||||
if (!accessPolicy.allowed || accessPolicy.error) {
|
||||
logger.debug(`User not allowed by org access policy because`, {
|
||||
|
||||
Reference in New Issue
Block a user