mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-07 02:20:34 +00:00
enforce max session length
This commit is contained in:
@@ -19,6 +19,13 @@ import {
|
||||
FormMessage
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -46,11 +53,24 @@ import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
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
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string(),
|
||||
subnet: z.string().optional(),
|
||||
requireTwoFactor: z.boolean().optional()
|
||||
requireTwoFactor: z.boolean().optional(),
|
||||
maxSessionLengthHours: z.number().nullable().optional()
|
||||
});
|
||||
|
||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||
@@ -76,7 +96,8 @@ export default function GeneralPage() {
|
||||
defaultValues: {
|
||||
name: org?.org.name,
|
||||
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"
|
||||
});
|
||||
@@ -141,6 +162,7 @@ export default function GeneralPage() {
|
||||
} as any;
|
||||
if (build !== "oss") {
|
||||
reqData.requireTwoFactor = data.requireTwoFactor || false;
|
||||
reqData.maxSessionLengthHours = data.maxSessionLengthHours;
|
||||
}
|
||||
|
||||
// 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>
|
||||
</SettingsSectionForm>
|
||||
|
||||
@@ -16,6 +16,8 @@ import Enable2FaDialog from "./Enable2FaDialog";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
|
||||
type OrgPolicyResultProps = {
|
||||
orgId: string;
|
||||
@@ -41,15 +43,18 @@ export default function OrgPolicyResult({
|
||||
const t = useTranslations();
|
||||
const { user } = useUserContext();
|
||||
const router = useRouter();
|
||||
|
||||
// Determine if user is compliant with 2FA policy
|
||||
const isTwoFactorCompliant = user?.twoFactorEnabled || false;
|
||||
const policyKeys = Object.keys(accessRes.policies || {});
|
||||
let requireedSteps = 0;
|
||||
let completedSteps = 0;
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
|
||||
const policies: PolicyItem[] = [];
|
||||
|
||||
// Only add 2FA policy if the organization has it enforced
|
||||
if (policyKeys.includes("requiredTwoFactor")) {
|
||||
if (
|
||||
accessRes.policies?.requiredTwoFactor === false ||
|
||||
accessRes.policies?.requiredTwoFactor === true
|
||||
) {
|
||||
const isTwoFactorCompliant =
|
||||
accessRes.policies?.requiredTwoFactor === true;
|
||||
policies.push({
|
||||
id: "two-factor",
|
||||
name: t("twoFactorAuthentication"),
|
||||
@@ -60,54 +65,51 @@ export default function OrgPolicyResult({
|
||||
: undefined,
|
||||
actionText: !isTwoFactorCompliant ? t("enableTwoFactor") : undefined
|
||||
});
|
||||
|
||||
// policies.push({
|
||||
// id: "reauth-required",
|
||||
// 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"
|
||||
// });
|
||||
requireedSteps += 1;
|
||||
if (isTwoFactorCompliant) {
|
||||
completedSteps += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const nonCompliantPolicies = policies.filter((policy) => !policy.compliant);
|
||||
const allCompliant =
|
||||
policies.length === 0 || nonCompliantPolicies.length === 0;
|
||||
// Add max session length policy if the organization has it enforced
|
||||
if (accessRes.policies?.maxSessionLength) {
|
||||
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 =
|
||||
totalPolicies > 0 ? (completedPolicies / totalPolicies) * 100 : 100;
|
||||
requireedSteps === 0 ? 100 : (completedSteps / requireedSteps) * 100;
|
||||
|
||||
// If no policies are enforced, show a simple success message
|
||||
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>
|
||||
);
|
||||
}
|
||||
const allCompliant = completedSteps === requireedSteps;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -123,12 +125,10 @@ export default function OrgPolicyResult({
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="px-6 pb-4">
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
|
||||
<span>
|
||||
{completedPolicies} of {totalPolicies} steps
|
||||
completed
|
||||
{completedSteps} of {requireedSteps} steps completed
|
||||
</span>
|
||||
<span>{Math.round(progressPercentage)}%</span>
|
||||
</div>
|
||||
@@ -172,17 +172,6 @@ export default function OrgPolicyResult({
|
||||
</CardContent>
|
||||
</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
|
||||
open={show2FaDialog}
|
||||
setOpen={(val) => {
|
||||
|
||||
Reference in New Issue
Block a user