mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-02 00:36:38 +00:00
enforce max session length
This commit is contained in:
@@ -1341,7 +1341,7 @@
|
|||||||
"twoFactorRequired": "Two-factor authentication is required to register a security key.",
|
"twoFactorRequired": "Two-factor authentication is required to register a security key.",
|
||||||
"twoFactor": "Two-Factor Authentication",
|
"twoFactor": "Two-Factor Authentication",
|
||||||
"twoFactorAuthentication": "Two-Factor Authentication",
|
"twoFactorAuthentication": "Two-Factor Authentication",
|
||||||
"twoFactorDescription": "Add an extra layer of security to your account with two-factor authentication",
|
"twoFactorDescription": "This organization requires two-factor authentication.",
|
||||||
"enableTwoFactor": "Enable Two-Factor Authentication",
|
"enableTwoFactor": "Enable Two-Factor Authentication",
|
||||||
"organizationSecurityPolicy": "Organization Security Policy",
|
"organizationSecurityPolicy": "Organization Security Policy",
|
||||||
"organizationSecurityPolicyDescription": "This organization has security requirements that must be met before you can access it",
|
"organizationSecurityPolicyDescription": "This organization has security requirements that must be met before you can access it",
|
||||||
@@ -1349,6 +1349,9 @@
|
|||||||
"allRequirementsMet": "All requirements have been met",
|
"allRequirementsMet": "All requirements have been met",
|
||||||
"completeRequirementsToContinue": "Complete the requirements below to continue accessing this organization",
|
"completeRequirementsToContinue": "Complete the requirements below to continue accessing this organization",
|
||||||
"youCanNowAccessOrganization": "You can now access this organization",
|
"youCanNowAccessOrganization": "You can now access this organization",
|
||||||
|
"reauthenticationRequired": "Session Length",
|
||||||
|
"reauthenticationDescription": "This organization requires you to log in every {maxDays} days.",
|
||||||
|
"reauthenticateNow": "Log In Again",
|
||||||
"adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.",
|
"adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.",
|
||||||
"securityKeyAdd": "Add Security Key",
|
"securityKeyAdd": "Add Security Key",
|
||||||
"securityKeyRegisterTitle": "Register New Security Key",
|
"securityKeyRegisterTitle": "Register New Security Key",
|
||||||
@@ -1746,6 +1749,10 @@
|
|||||||
"requireTwoFactorDescription": "When enabled, all internal users in this organization must have two-factor authentication enabled to access the organization.",
|
"requireTwoFactorDescription": "When enabled, all internal users in this organization must have two-factor authentication enabled to access the organization.",
|
||||||
"requireTwoFactorDisabledDescription": "This feature requires a valid license (Enterprise) or active subscription (SaaS)",
|
"requireTwoFactorDisabledDescription": "This feature requires a valid license (Enterprise) or active subscription (SaaS)",
|
||||||
"requireTwoFactorCannotEnableDescription": "You must enable two-factor authentication for your account before enforcing it for all users",
|
"requireTwoFactorCannotEnableDescription": "You must enable two-factor authentication for your account before enforcing it for all users",
|
||||||
|
"maxSessionLength": "Maximum Session Length",
|
||||||
|
"maxSessionLengthDescription": "Set the maximum duration for user sessions. After this time, users will need to re-authenticate.",
|
||||||
|
"maxSessionLengthDisabledDescription": "This feature requires a valid license (Enterprise) or active subscription (SaaS)",
|
||||||
|
"selectSessionLength": "Select session length",
|
||||||
"subscriptionBadge": "Subscription Required",
|
"subscriptionBadge": "Subscription Required",
|
||||||
"authPageErrorUpdateMessage": "An error occurred while updating the auth page settings",
|
"authPageErrorUpdateMessage": "An error occurred while updating the auth page settings",
|
||||||
"authPageUpdated": "Auth page updated successfully",
|
"authPageUpdated": "Auth page updated successfully",
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ export async function createSession(
|
|||||||
const session: Session = {
|
const session: Session = {
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
userId,
|
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);
|
await db.insert(sessions).values(session);
|
||||||
return session;
|
return session;
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ export const orgs = pgTable("orgs", {
|
|||||||
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)
|
requireTwoFactor: boolean("requireTwoFactor"),
|
||||||
|
maxSessionLengthHours: integer("maxSessionLengthHours")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgDomains = pgTable("orgDomains", {
|
export const orgDomains = pgTable("orgDomains", {
|
||||||
@@ -226,7 +227,8 @@ export const sessions = pgTable("session", {
|
|||||||
userId: varchar("userId")
|
userId: varchar("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, { onDelete: "cascade" }),
|
.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", {
|
export const newtSessions = pgTable("newtSession", {
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export const orgs = sqliteTable("orgs", {
|
|||||||
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" })
|
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }),
|
||||||
|
maxSessionLengthHours: integer("maxSessionLengthHours") // hours
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userDomains = sqliteTable("userDomains", {
|
export const userDomains = sqliteTable("userDomains", {
|
||||||
@@ -333,7 +334,8 @@ export const sessions = sqliteTable("session", {
|
|||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, { onDelete: "cascade" }),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
expiresAt: integer("expiresAt").notNull()
|
expiresAt: integer("expiresAt").notNull(),
|
||||||
|
issuedAt: integer("issuedAt")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const newtSessions = sqliteTable("newtSession", {
|
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 = {
|
export type CheckOrgAccessPolicyProps = {
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
org?: Org;
|
org?: Org;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
user?: User;
|
user?: User;
|
||||||
|
sessionId?: string;
|
||||||
|
session?: Session;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CheckOrgAccessPolicyResult = {
|
export type CheckOrgAccessPolicyResult = {
|
||||||
@@ -12,6 +14,11 @@ export type CheckOrgAccessPolicyResult = {
|
|||||||
error?: string;
|
error?: string;
|
||||||
policies?: {
|
policies?: {
|
||||||
requiredTwoFactor?: boolean;
|
requiredTwoFactor?: boolean;
|
||||||
|
maxSessionLength?: {
|
||||||
|
compliant: boolean;
|
||||||
|
maxSessionLengthHours: number;
|
||||||
|
sessionAgeHours: number;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ export async function verifyOrgAccess(
|
|||||||
|
|
||||||
const policyCheck = await checkOrgAccessPolicy({
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
orgId,
|
orgId,
|
||||||
userId
|
userId,
|
||||||
|
session: req.session
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug("Org check policy result", { policyCheck });
|
logger.debug("Org check policy result", { policyCheck });
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { build } from "@server/build";
|
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 { 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";
|
||||||
@@ -28,6 +28,7 @@ export async function checkOrgAccessPolicy(
|
|||||||
): Promise<CheckOrgAccessPolicyResult> {
|
): Promise<CheckOrgAccessPolicyResult> {
|
||||||
const userId = props.userId || props.user?.userId;
|
const userId = props.userId || props.user?.userId;
|
||||||
const orgId = props.orgId || props.org?.orgId;
|
const orgId = props.orgId || props.org?.orgId;
|
||||||
|
const sessionId = props.sessionId || props.session?.sessionId;
|
||||||
|
|
||||||
if (!orgId) {
|
if (!orgId) {
|
||||||
return {
|
return {
|
||||||
@@ -38,6 +39,9 @@ export async function checkOrgAccessPolicy(
|
|||||||
if (!userId) {
|
if (!userId) {
|
||||||
return { allowed: false, error: "User ID is required" };
|
return { allowed: false, error: "User ID is required" };
|
||||||
}
|
}
|
||||||
|
if (!sessionId) {
|
||||||
|
return { allowed: false, error: "Session ID is required" };
|
||||||
|
}
|
||||||
|
|
||||||
if (build === "saas") {
|
if (build === "saas") {
|
||||||
const { tier } = await getOrgTierData(orgId);
|
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
|
// now check the policies
|
||||||
const policies: CheckOrgAccessPolicyResult["policies"] = {};
|
const policies: CheckOrgAccessPolicyResult["policies"] = {};
|
||||||
|
|
||||||
@@ -88,7 +103,34 @@ export async function checkOrgAccessPolicy(
|
|||||||
policies.requiredTwoFactor = props.user.twoFactorEnabled || false;
|
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 {
|
return {
|
||||||
allowed,
|
allowed,
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ export async function checkOrgUserAccess(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
logger.debug("here0 ")
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
if (!parsedParams.success) {
|
if (!parsedParams.success) {
|
||||||
return next(
|
return next(
|
||||||
@@ -116,7 +115,8 @@ export async function checkOrgUserAccess(
|
|||||||
|
|
||||||
const policyCheck = await checkOrgAccessPolicy({
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
orgId,
|
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
|
// 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
|
const updateOrgBodySchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(255).optional(),
|
name: z.string().min(1).max(255).optional(),
|
||||||
requireTwoFactor: z.boolean().optional()
|
requireTwoFactor: z.boolean().optional(),
|
||||||
|
maxSessionLengthHours: z.number().nullable().optional()
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
@@ -80,6 +81,7 @@ export async function updateOrg(
|
|||||||
const isLicensed = await isLicensedOrSubscribed(orgId);
|
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||||
if (!isLicensed) {
|
if (!isLicensed) {
|
||||||
parsedBody.data.requireTwoFactor = undefined;
|
parsedBody.data.requireTwoFactor = undefined;
|
||||||
|
parsedBody.data.maxSessionLengthHours = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -100,7 +102,8 @@ export async function updateOrg(
|
|||||||
.update(orgs)
|
.update(orgs)
|
||||||
.set({
|
.set({
|
||||||
name: parsedBody.data.name,
|
name: parsedBody.data.name,
|
||||||
requireTwoFactor: parsedBody.data.requireTwoFactor
|
requireTwoFactor: parsedBody.data.requireTwoFactor,
|
||||||
|
maxSessionLengthHours: parsedBody.data.maxSessionLengthHours
|
||||||
})
|
})
|
||||||
.where(eq(orgs.orgId, orgId))
|
.where(eq(orgs.orgId, orgId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ export async function getExchangeToken(
|
|||||||
// check org policy here
|
// check org policy here
|
||||||
const hasAccess = await checkOrgAccessPolicy({
|
const hasAccess = await checkOrgAccessPolicy({
|
||||||
orgId: resource[0].orgId,
|
orgId: resource[0].orgId,
|
||||||
userId: req.user!.userId
|
userId: req.user!.userId,
|
||||||
|
session: req.session
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!hasAccess.allowed || hasAccess.error) {
|
if (!hasAccess.allowed || hasAccess.error) {
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ import {
|
|||||||
FormMessage
|
FormMessage
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -46,11 +53,24 @@ import { SwitchInput } from "@app/components/SwitchInput";
|
|||||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||||
import { Badge } from "@app/components/ui/badge";
|
import { Badge } from "@app/components/ui/badge";
|
||||||
|
|
||||||
|
// Session length options in hours
|
||||||
|
const SESSION_LENGTH_OPTIONS = [
|
||||||
|
{ value: null, label: "Unenforced" },
|
||||||
|
{ value: 72, label: "3 days" }, // 3 * 24 = 72 hours
|
||||||
|
{ value: 168, label: "7 days" }, // 7 * 24 = 168 hours
|
||||||
|
{ value: 336, label: "14 days" }, // 14 * 24 = 336 hours
|
||||||
|
{ value: 720, label: "30 days" }, // 30 * 24 = 720 hours
|
||||||
|
{ value: 2160, label: "90 days" }, // 90 * 24 = 2160 hours
|
||||||
|
{ value: 4320, label: "180 days" } // 180 * 24 = 4320 hours
|
||||||
|
];
|
||||||
|
|
||||||
// Schema for general organization settings
|
// Schema for general organization settings
|
||||||
const GeneralFormSchema = z.object({
|
const GeneralFormSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
subnet: z.string().optional(),
|
subnet: z.string().optional(),
|
||||||
requireTwoFactor: z.boolean().optional()
|
requireTwoFactor: z.boolean().optional(),
|
||||||
|
maxSessionLengthHours: z.number().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||||
@@ -76,7 +96,8 @@ export default function GeneralPage() {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: org?.org.name,
|
name: org?.org.name,
|
||||||
subnet: org?.org.subnet || "", // Add default value for subnet
|
subnet: org?.org.subnet || "", // Add default value for subnet
|
||||||
requireTwoFactor: org?.org.requireTwoFactor || false
|
requireTwoFactor: org?.org.requireTwoFactor || false,
|
||||||
|
maxSessionLengthHours: org?.org.maxSessionLengthHours || null
|
||||||
},
|
},
|
||||||
mode: "onChange"
|
mode: "onChange"
|
||||||
});
|
});
|
||||||
@@ -141,6 +162,7 @@ export default function GeneralPage() {
|
|||||||
} as any;
|
} as any;
|
||||||
if (build !== "oss") {
|
if (build !== "oss") {
|
||||||
reqData.requireTwoFactor = data.requireTwoFactor || false;
|
reqData.requireTwoFactor = data.requireTwoFactor || false;
|
||||||
|
reqData.maxSessionLengthHours = data.maxSessionLengthHours;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update organization
|
// Update organization
|
||||||
@@ -337,6 +359,97 @@ export default function GeneralPage() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="maxSessionLengthHours"
|
||||||
|
render={({ field }) => {
|
||||||
|
const isEnterpriseNotLicensed =
|
||||||
|
build === "enterprise" &&
|
||||||
|
!isUnlocked();
|
||||||
|
const isSaasNotSubscribed =
|
||||||
|
build === "saas" &&
|
||||||
|
!subscriptionStatus?.isSubscribed();
|
||||||
|
const isDisabled =
|
||||||
|
isEnterpriseNotLicensed ||
|
||||||
|
isSaasNotSubscribed;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem className="col-span-2">
|
||||||
|
<FormLabel>
|
||||||
|
{t("maxSessionLength")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
field.value?.toString() ||
|
||||||
|
"null"
|
||||||
|
}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (!isDisabled) {
|
||||||
|
const numValue =
|
||||||
|
value === "null"
|
||||||
|
? null
|
||||||
|
: parseInt(
|
||||||
|
value,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"maxSessionLengthHours",
|
||||||
|
numValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
t(
|
||||||
|
"selectSessionLength"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{SESSION_LENGTH_OPTIONS.map(
|
||||||
|
(option) => (
|
||||||
|
<SelectItem
|
||||||
|
key={
|
||||||
|
option.value ===
|
||||||
|
null
|
||||||
|
? "null"
|
||||||
|
: option.value.toString()
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
option.value ===
|
||||||
|
null
|
||||||
|
? "null"
|
||||||
|
: option.value.toString()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
option.label
|
||||||
|
}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<FormDescription>
|
||||||
|
{isDisabled
|
||||||
|
? t(
|
||||||
|
"maxSessionLengthDisabledDescription"
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"maxSessionLengthDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import Enable2FaDialog from "./Enable2FaDialog";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useUserContext } from "@app/hooks/useUserContext";
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { createApiClient } from "@app/lib/api";
|
||||||
|
|
||||||
type OrgPolicyResultProps = {
|
type OrgPolicyResultProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -41,15 +43,18 @@ export default function OrgPolicyResult({
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { user } = useUserContext();
|
const { user } = useUserContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
let requireedSteps = 0;
|
||||||
// Determine if user is compliant with 2FA policy
|
let completedSteps = 0;
|
||||||
const isTwoFactorCompliant = user?.twoFactorEnabled || false;
|
const { env } = useEnvContext();
|
||||||
const policyKeys = Object.keys(accessRes.policies || {});
|
const api = createApiClient({ env });
|
||||||
|
|
||||||
const policies: PolicyItem[] = [];
|
const policies: PolicyItem[] = [];
|
||||||
|
if (
|
||||||
// Only add 2FA policy if the organization has it enforced
|
accessRes.policies?.requiredTwoFactor === false ||
|
||||||
if (policyKeys.includes("requiredTwoFactor")) {
|
accessRes.policies?.requiredTwoFactor === true
|
||||||
|
) {
|
||||||
|
const isTwoFactorCompliant =
|
||||||
|
accessRes.policies?.requiredTwoFactor === true;
|
||||||
policies.push({
|
policies.push({
|
||||||
id: "two-factor",
|
id: "two-factor",
|
||||||
name: t("twoFactorAuthentication"),
|
name: t("twoFactorAuthentication"),
|
||||||
@@ -60,54 +65,51 @@ export default function OrgPolicyResult({
|
|||||||
: undefined,
|
: undefined,
|
||||||
actionText: !isTwoFactorCompliant ? t("enableTwoFactor") : undefined
|
actionText: !isTwoFactorCompliant ? t("enableTwoFactor") : undefined
|
||||||
});
|
});
|
||||||
|
requireedSteps += 1;
|
||||||
// policies.push({
|
if (isTwoFactorCompliant) {
|
||||||
// id: "reauth-required",
|
completedSteps += 1;
|
||||||
// name: "Re-authentication",
|
}
|
||||||
// description:
|
|
||||||
// "It's been 30 days since you last verified your identity. Please log out and log back in to continue.",
|
|
||||||
// compliant: false,
|
|
||||||
// action: () => {},
|
|
||||||
// actionText: "Log Out"
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// policies.push({
|
|
||||||
// id: "password-rotation",
|
|
||||||
// name: "Password Rotation",
|
|
||||||
// description:
|
|
||||||
// "It's been 30 days since you last changed your password. Please update your password to continue.",
|
|
||||||
// compliant: false,
|
|
||||||
// action: () => {},
|
|
||||||
// actionText: "Change Password"
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const nonCompliantPolicies = policies.filter((policy) => !policy.compliant);
|
// Add max session length policy if the organization has it enforced
|
||||||
const allCompliant =
|
if (accessRes.policies?.maxSessionLength) {
|
||||||
policies.length === 0 || nonCompliantPolicies.length === 0;
|
const maxSessionPolicy = accessRes.policies?.maxSessionLength;
|
||||||
|
const maxDays = Math.round(maxSessionPolicy.maxSessionLengthHours / 24);
|
||||||
|
const daysAgo = Math.round(maxSessionPolicy.sessionAgeHours / 24);
|
||||||
|
|
||||||
|
policies.push({
|
||||||
|
id: "max-session-length",
|
||||||
|
name: t("reauthenticationRequired"),
|
||||||
|
description: t("reauthenticationDescription", {
|
||||||
|
maxDays,
|
||||||
|
daysAgo
|
||||||
|
}),
|
||||||
|
compliant: maxSessionPolicy.compliant,
|
||||||
|
action: !maxSessionPolicy.compliant
|
||||||
|
? async () => {
|
||||||
|
try {
|
||||||
|
await api.post("/auth/logout", undefined);
|
||||||
|
router.push("/auth/login");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during logout:", error);
|
||||||
|
router.push("/auth/login");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
actionText: !maxSessionPolicy.compliant
|
||||||
|
? t("reauthenticateNow")
|
||||||
|
: undefined
|
||||||
|
});
|
||||||
|
requireedSteps += 1;
|
||||||
|
if (maxSessionPolicy.compliant) {
|
||||||
|
completedSteps += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate progress
|
|
||||||
const completedPolicies = policies.filter(
|
|
||||||
(policy) => policy.compliant
|
|
||||||
).length;
|
|
||||||
const totalPolicies = policies.length;
|
|
||||||
const progressPercentage =
|
const progressPercentage =
|
||||||
totalPolicies > 0 ? (completedPolicies / totalPolicies) * 100 : 100;
|
requireedSteps === 0 ? 100 : (completedSteps / requireedSteps) * 100;
|
||||||
|
|
||||||
// If no policies are enforced, show a simple success message
|
const allCompliant = completedSteps === requireedSteps;
|
||||||
if (policies.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<CheckCircle2 className="mx-auto h-12 w-12 text-green-500 mb-4" />
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-2">
|
|
||||||
{t("accessGranted")}
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{t("noSecurityRequirements")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -123,12 +125,10 @@ export default function OrgPolicyResult({
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{/* Progress Bar */}
|
|
||||||
<div className="px-6 pb-4">
|
<div className="px-6 pb-4">
|
||||||
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
|
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
|
||||||
<span>
|
<span>
|
||||||
{completedPolicies} of {totalPolicies} steps
|
{completedSteps} of {requireedSteps} steps completed
|
||||||
completed
|
|
||||||
</span>
|
</span>
|
||||||
<span>{Math.round(progressPercentage)}%</span>
|
<span>{Math.round(progressPercentage)}%</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,17 +172,6 @@ export default function OrgPolicyResult({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{allCompliant && (
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-sm text-green-600 font-medium">
|
|
||||||
{t("allRequirementsMet")}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
{t("youCanNowAccessOrganization")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Enable2FaDialog
|
<Enable2FaDialog
|
||||||
open={show2FaDialog}
|
open={show2FaDialog}
|
||||||
setOpen={(val) => {
|
setOpen={(val) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user