mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-04 01:36:39 +00:00
enforce resource session length
This commit is contained in:
@@ -50,7 +50,8 @@ export async function createResourceSession(opts: {
|
|||||||
doNotExtend: opts.doNotExtend || false,
|
doNotExtend: opts.doNotExtend || false,
|
||||||
accessTokenId: opts.accessTokenId || null,
|
accessTokenId: opts.accessTokenId || null,
|
||||||
isRequestToken: opts.isRequestToken || false,
|
isRequestToken: opts.isRequestToken || false,
|
||||||
userSessionId: opts.userSessionId || null
|
userSessionId: opts.userSessionId || null,
|
||||||
|
issuedAt: new Date().getTime()
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.insert(resourceSessions).values(session);
|
await db.insert(resourceSessions).values(session);
|
||||||
|
|||||||
@@ -447,7 +447,8 @@ export const resourceSessions = pgTable("resourceSessions", {
|
|||||||
{
|
{
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
issuedAt: bigint("issuedAt", { mode: "number" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resourceWhitelist = pgTable("resourceWhitelist", {
|
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 {
|
import {
|
||||||
Resource,
|
Resource,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
@@ -23,6 +23,7 @@ export type ResourceWithAuth = {
|
|||||||
pincode: ResourcePincode | null;
|
pincode: ResourcePincode | null;
|
||||||
password: ResourcePassword | null;
|
password: ResourcePassword | null;
|
||||||
headerAuth: ResourceHeaderAuth | null;
|
headerAuth: ResourceHeaderAuth | null;
|
||||||
|
org: Org;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserSessionWithUser = {
|
export type UserSessionWithUser = {
|
||||||
@@ -51,6 +52,10 @@ export async function getResourceByDomain(
|
|||||||
resourceHeaderAuth,
|
resourceHeaderAuth,
|
||||||
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
||||||
)
|
)
|
||||||
|
.innerJoin(
|
||||||
|
orgs,
|
||||||
|
eq(orgs.orgId, resources.orgId)
|
||||||
|
)
|
||||||
.where(eq(resources.fullDomain, domain))
|
.where(eq(resources.fullDomain, domain))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
@@ -62,7 +67,8 @@ export async function getResourceByDomain(
|
|||||||
resource: result.resources,
|
resource: result.resources,
|
||||||
pincode: result.resourcePincode,
|
pincode: result.resourcePincode,
|
||||||
password: result.resourcePassword,
|
password: result.resourcePassword,
|
||||||
headerAuth: result.resourceHeaderAuth
|
headerAuth: result.resourceHeaderAuth,
|
||||||
|
org: result.orgs
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -587,7 +587,8 @@ export const resourceSessions = sqliteTable("resourceSessions", {
|
|||||||
{
|
{
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
issuedAt: integer("issuedAt")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resourceWhitelist = sqliteTable("resourceWhitelist", {
|
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 = {
|
export type CheckOrgAccessPolicyProps = {
|
||||||
orgId?: string;
|
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(
|
export async function checkOrgAccessPolicy(
|
||||||
props: CheckOrgAccessPolicyProps
|
props: CheckOrgAccessPolicyProps
|
||||||
): Promise<{
|
): Promise<{
|
||||||
|
|||||||
@@ -12,7 +12,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { build } from "@server/build";
|
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 { getOrgTierData } from "#private/lib/billing";
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
import license from "#private/license/license";
|
import license from "#private/license/license";
|
||||||
@@ -23,6 +30,35 @@ import {
|
|||||||
} from "@server/lib/checkOrgAccessPolicy";
|
} from "@server/lib/checkOrgAccessPolicy";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
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(
|
export async function checkOrgAccessPolicy(
|
||||||
props: CheckOrgAccessPolicyProps
|
props: CheckOrgAccessPolicyProps
|
||||||
): Promise<CheckOrgAccessPolicyResult> {
|
): Promise<CheckOrgAccessPolicyResult> {
|
||||||
@@ -43,15 +79,6 @@ export async function checkOrgAccessPolicy(
|
|||||||
return { allowed: false, error: "Session ID is required" };
|
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") {
|
if (build === "enterprise") {
|
||||||
const isUnlocked = await license.isUnlocked();
|
const isUnlocked = await license.isUnlocked();
|
||||||
// if not licensed, don't check the policies
|
// 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 { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
|
||||||
import { maxmindLookup } from "@server/db/maxmind";
|
import { maxmindLookup } from "@server/db/maxmind";
|
||||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
|
||||||
import { CheckOrgAccessPolicyResult } from "@server/lib/checkOrgAccessPolicy";
|
|
||||||
|
|
||||||
// Zod schemas for request validation
|
// Zod schemas for request validation
|
||||||
const getResourceByDomainParamsSchema = z
|
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";
|
} from "@server/db/queries/verifySessionQueries";
|
||||||
import {
|
import {
|
||||||
LoginPage,
|
LoginPage,
|
||||||
|
Org,
|
||||||
Resource,
|
Resource,
|
||||||
ResourceAccessToken,
|
ResourceAccessToken,
|
||||||
ResourceHeaderAuth,
|
ResourceHeaderAuth,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
ResourcePincode,
|
ResourcePincode,
|
||||||
ResourceRule
|
ResourceRule,
|
||||||
|
resourceSessions
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { isIpInCidr } from "@server/lib/ip";
|
import { isIpInCidr } from "@server/lib/ip";
|
||||||
@@ -37,7 +39,10 @@ import { getCountryCodeForIp } from "@server/lib/geoip";
|
|||||||
import { getOrgTierData } from "#dynamic/lib/billing";
|
import { getOrgTierData } from "#dynamic/lib/billing";
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
import { verifyPassword } from "@server/auth/password";
|
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
|
// We'll see if this speeds anything up
|
||||||
const cache = new NodeCache({
|
const cache = new NodeCache({
|
||||||
@@ -142,6 +147,7 @@ export async function verifyResourceSession(
|
|||||||
pincode: ResourcePincode | null;
|
pincode: ResourcePincode | null;
|
||||||
password: ResourcePassword | null;
|
password: ResourcePassword | null;
|
||||||
headerAuth: ResourceHeaderAuth | null;
|
headerAuth: ResourceHeaderAuth | null;
|
||||||
|
org: Org;
|
||||||
}
|
}
|
||||||
| undefined = cache.get(resourceCacheKey);
|
| undefined = cache.get(resourceCacheKey);
|
||||||
|
|
||||||
@@ -380,6 +386,22 @@ export async function verifyResourceSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (resourceSession) {
|
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) {
|
if (pincode && resourceSession.pincodeId) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Resource allowed because pincode session is valid"
|
"Resource allowed because pincode session is valid"
|
||||||
@@ -422,7 +444,8 @@ export async function verifyResourceSession(
|
|||||||
if (allowedUserData === undefined) {
|
if (allowedUserData === undefined) {
|
||||||
allowedUserData = await isUserAllowedToAccessResource(
|
allowedUserData = await isUserAllowedToAccessResource(
|
||||||
resourceSession.userSessionId,
|
resourceSession.userSessionId,
|
||||||
resource
|
resource,
|
||||||
|
resourceData.org
|
||||||
);
|
);
|
||||||
|
|
||||||
cache.set(userAccessCacheKey, allowedUserData);
|
cache.set(userAccessCacheKey, allowedUserData);
|
||||||
@@ -564,7 +587,8 @@ function allowed(res: Response, userData?: BasicUserData) {
|
|||||||
|
|
||||||
async function isUserAllowedToAccessResource(
|
async function isUserAllowedToAccessResource(
|
||||||
userSessionId: string,
|
userSessionId: string,
|
||||||
resource: Resource
|
resource: Resource,
|
||||||
|
org: Org
|
||||||
): Promise<BasicUserData | null> {
|
): Promise<BasicUserData | null> {
|
||||||
const result = await getUserSessionWithUser(userSessionId);
|
const result = await getUserSessionWithUser(userSessionId);
|
||||||
|
|
||||||
@@ -592,9 +616,9 @@ async function isUserAllowedToAccessResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const accessPolicy = await checkOrgAccessPolicy({
|
const accessPolicy = await checkOrgAccessPolicy({
|
||||||
orgId: resource.orgId,
|
org,
|
||||||
userId: user.userId,
|
user,
|
||||||
sessionId: session.sessionId
|
session
|
||||||
});
|
});
|
||||||
if (!accessPolicy.allowed || accessPolicy.error) {
|
if (!accessPolicy.allowed || accessPolicy.error) {
|
||||||
logger.debug(`User not allowed by org access policy because`, {
|
logger.debug(`User not allowed by org access policy because`, {
|
||||||
|
|||||||
Reference in New Issue
Block a user