mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-13 08:26:40 +00:00
enforce max session length
This commit is contained in:
@@ -39,7 +39,8 @@ export async function createSession(
|
||||
const session: Session = {
|
||||
sessionId: sessionId,
|
||||
userId,
|
||||
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime()
|
||||
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
||||
issuedAt: new Date().getTime()
|
||||
};
|
||||
await db.insert(sessions).values(session);
|
||||
return session;
|
||||
|
||||
@@ -26,7 +26,8 @@ export const orgs = pgTable("orgs", {
|
||||
name: varchar("name").notNull(),
|
||||
subnet: varchar("subnet"),
|
||||
createdAt: text("createdAt"),
|
||||
requireTwoFactor: boolean("requireTwoFactor").default(false)
|
||||
requireTwoFactor: boolean("requireTwoFactor"),
|
||||
maxSessionLengthHours: integer("maxSessionLengthHours")
|
||||
});
|
||||
|
||||
export const orgDomains = pgTable("orgDomains", {
|
||||
@@ -226,7 +227,8 @@ export const sessions = pgTable("session", {
|
||||
userId: varchar("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
|
||||
issuedAt: bigint("expiresAt", { mode: "number" })
|
||||
});
|
||||
|
||||
export const newtSessions = pgTable("newtSession", {
|
||||
|
||||
@@ -19,7 +19,8 @@ export const orgs = sqliteTable("orgs", {
|
||||
name: text("name").notNull(),
|
||||
subnet: text("subnet"),
|
||||
createdAt: text("createdAt"),
|
||||
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" })
|
||||
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }),
|
||||
maxSessionLengthHours: integer("maxSessionLengthHours") // hours
|
||||
});
|
||||
|
||||
export const userDomains = sqliteTable("userDomains", {
|
||||
@@ -333,7 +334,8 @@ export const sessions = sqliteTable("session", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.userId, { onDelete: "cascade" }),
|
||||
expiresAt: integer("expiresAt").notNull()
|
||||
expiresAt: integer("expiresAt").notNull(),
|
||||
issuedAt: integer("issuedAt")
|
||||
});
|
||||
|
||||
export const newtSessions = sqliteTable("newtSession", {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Org, User } from "@server/db";
|
||||
import { Org, Session, User } from "@server/db";
|
||||
|
||||
export type CheckOrgAccessPolicyProps = {
|
||||
orgId?: string;
|
||||
org?: Org;
|
||||
userId?: string;
|
||||
user?: User;
|
||||
sessionId?: string;
|
||||
session?: Session;
|
||||
};
|
||||
|
||||
export type CheckOrgAccessPolicyResult = {
|
||||
@@ -12,6 +14,11 @@ export type CheckOrgAccessPolicyResult = {
|
||||
error?: string;
|
||||
policies?: {
|
||||
requiredTwoFactor?: boolean;
|
||||
maxSessionLength?: {
|
||||
compliant: boolean;
|
||||
maxSessionLengthHours: number;
|
||||
sessionAgeHours: number;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ export async function verifyOrgAccess(
|
||||
|
||||
const policyCheck = await checkOrgAccessPolicy({
|
||||
orgId,
|
||||
userId
|
||||
userId,
|
||||
session: req.session
|
||||
});
|
||||
|
||||
logger.debug("Org check policy result", { policyCheck });
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import { build } from "@server/build";
|
||||
import { db, Org, orgs, User, users } from "@server/db";
|
||||
import { db, Org, orgs, sessions, User, users } from "@server/db";
|
||||
import { getOrgTierData } from "#private/lib/billing";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
import license from "#private/license/license";
|
||||
@@ -28,6 +28,7 @@ export async function checkOrgAccessPolicy(
|
||||
): Promise<CheckOrgAccessPolicyResult> {
|
||||
const userId = props.userId || props.user?.userId;
|
||||
const orgId = props.orgId || props.org?.orgId;
|
||||
const sessionId = props.sessionId || props.session?.sessionId;
|
||||
|
||||
if (!orgId) {
|
||||
return {
|
||||
@@ -38,6 +39,9 @@ export async function checkOrgAccessPolicy(
|
||||
if (!userId) {
|
||||
return { allowed: false, error: "User ID is required" };
|
||||
}
|
||||
if (!sessionId) {
|
||||
return { allowed: false, error: "Session ID is required" };
|
||||
}
|
||||
|
||||
if (build === "saas") {
|
||||
const { tier } = await getOrgTierData(orgId);
|
||||
@@ -80,6 +84,17 @@ export async function checkOrgAccessPolicy(
|
||||
}
|
||||
}
|
||||
|
||||
if (!props.session) {
|
||||
const [sessionQuery] = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
.where(eq(sessions.sessionId, sessionId));
|
||||
props.session = sessionQuery;
|
||||
if (!props.session) {
|
||||
return { allowed: false, error: "Session not found" };
|
||||
}
|
||||
}
|
||||
|
||||
// now check the policies
|
||||
const policies: CheckOrgAccessPolicyResult["policies"] = {};
|
||||
|
||||
@@ -88,7 +103,34 @@ export async function checkOrgAccessPolicy(
|
||||
policies.requiredTwoFactor = props.user.twoFactorEnabled || false;
|
||||
}
|
||||
|
||||
const allowed = Object.values(policies).every((v) => v === true);
|
||||
if (props.org.maxSessionLengthHours) {
|
||||
const sessionIssuedAt = props.session.issuedAt; // may be null
|
||||
const maxSessionLengthHours = props.org.maxSessionLengthHours;
|
||||
|
||||
if (sessionIssuedAt) {
|
||||
const maxSessionLengthMs = maxSessionLengthHours * 60 * 60 * 1000;
|
||||
const sessionAgeMs = Date.now() - sessionIssuedAt;
|
||||
policies.maxSessionLength = {
|
||||
compliant: sessionAgeMs <= maxSessionLengthMs,
|
||||
maxSessionLengthHours,
|
||||
sessionAgeHours: sessionAgeMs / (60 * 60 * 1000)
|
||||
};
|
||||
} else {
|
||||
policies.maxSessionLength = {
|
||||
compliant: false,
|
||||
maxSessionLengthHours,
|
||||
sessionAgeHours: maxSessionLengthHours
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let allowed = true;
|
||||
if (policies.requiredTwoFactor === false) {
|
||||
allowed = false;
|
||||
}
|
||||
if (policies.maxSessionLength && policies.maxSessionLength.compliant === false) {
|
||||
allowed = false;
|
||||
}
|
||||
|
||||
return {
|
||||
allowed,
|
||||
|
||||
@@ -68,7 +68,6 @@ export async function checkOrgUserAccess(
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
logger.debug("here0 ")
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
@@ -116,7 +115,8 @@ export async function checkOrgUserAccess(
|
||||
|
||||
const policyCheck = await checkOrgAccessPolicy({
|
||||
orgId,
|
||||
userId
|
||||
userId,
|
||||
session: req.session
|
||||
});
|
||||
|
||||
// if we get here, the user has an org join, we just don't know if they pass the policies
|
||||
|
||||
@@ -24,7 +24,8 @@ const updateOrgParamsSchema = z
|
||||
const updateOrgBodySchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
requireTwoFactor: z.boolean().optional()
|
||||
requireTwoFactor: z.boolean().optional(),
|
||||
maxSessionLengthHours: z.number().nullable().optional()
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
@@ -80,6 +81,7 @@ export async function updateOrg(
|
||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||
if (!isLicensed) {
|
||||
parsedBody.data.requireTwoFactor = undefined;
|
||||
parsedBody.data.maxSessionLengthHours = undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -100,7 +102,8 @@ export async function updateOrg(
|
||||
.update(orgs)
|
||||
.set({
|
||||
name: parsedBody.data.name,
|
||||
requireTwoFactor: parsedBody.data.requireTwoFactor
|
||||
requireTwoFactor: parsedBody.data.requireTwoFactor,
|
||||
maxSessionLengthHours: parsedBody.data.maxSessionLengthHours
|
||||
})
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.returning();
|
||||
|
||||
@@ -76,7 +76,8 @@ export async function getExchangeToken(
|
||||
// check org policy here
|
||||
const hasAccess = await checkOrgAccessPolicy({
|
||||
orgId: resource[0].orgId,
|
||||
userId: req.user!.userId
|
||||
userId: req.user!.userId,
|
||||
session: req.session
|
||||
});
|
||||
|
||||
if (!hasAccess.allowed || hasAccess.error) {
|
||||
|
||||
Reference in New Issue
Block a user