From 0900a5cc79a712003cd08675a99f0119bc53482c Mon Sep 17 00:00:00 2001 From: Dante Catalfamo <43040593+dantecatalfamo@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:52:57 -0500 Subject: [PATCH] Add activities and settings ui (#38495) **Related issue:** Resolves #37276 --- frontend/interfaces/activity.ts | 3 + frontend/interfaces/config.ts | 2 + .../GlobalActivityItem/GlobalActivityItem.tsx | 6 + .../ConditionalAccess/ConditionalAccess.tsx | 143 ++++++++++++++---- .../cards/ConditionalAccess/_styles.scss | 10 ++ 5 files changed, 138 insertions(+), 26 deletions(-) diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index b1bd761c24..8a9cd86a86 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -142,6 +142,7 @@ export enum ActivityType { DeletedMSEntraConditionalAccess = "deleted_conditional_access_integration_microsoft", AddedConditionalAccessOkta = "added_conditional_access_okta", DeletedConditionalAccessOkta = "deleted_conditional_access_okta", + UpdatedConditionalAccessBypass = "update_conditional_access_bypass", // enable/disable above feature for a team EnabledConditionalAccessAutomations = "enabled_conditional_access_automations", DisabledConditionalAccessAutomations = "disabled_conditional_access_automations", @@ -421,6 +422,8 @@ export const ACTIVITY_TYPE_TO_FILTER_LABEL: Record = { [ActivityType.EditedAndroidCertificate]: "GitOps: edited certificate templates: Android", [ActivityType.AddedConditionalAccessOkta]: "Added conditional access: Okta", + [ActivityType.UpdatedConditionalAccessBypass]: + "Updated conditional access experience", [ActivityType.DeletedConditionalAccessOkta]: "Deleted conditional access: Okta", [ActivityType.EditedSetupExperienceSoftware]: diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts index ed2dfc9562..041abaa045 100644 --- a/frontend/interfaces/config.ts +++ b/frontend/interfaces/config.ts @@ -167,6 +167,8 @@ export interface IConfig { okta_assertion_consumer_service_url: string; okta_audience_uri: string; okta_certificate: string; + // Bypass setting + bypass_disabled?: boolean; }; host_expiry_settings: { host_expiry_enabled: boolean; diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx index acfb5d83c0..66fb8eeabd 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx @@ -1379,6 +1379,9 @@ const TAGGED_TEMPLATES = { deletedConditionalAccessOkta: () => ( <> deleted Okta conditional access configuration. ), + updatedConditionalAccessBypass: () => ( + <> edited conditional access end user experience. + ), enabledConditionalAccessAutomations: (activity: IActivity) => { const teamName = activity.details?.team_name; return ( @@ -2023,6 +2026,9 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => { case ActivityType.DeletedConditionalAccessOkta: { return TAGGED_TEMPLATES.deletedConditionalAccessOkta(); } + case ActivityType.UpdatedConditionalAccessBypass: { + return TAGGED_TEMPLATES.updatedConditionalAccessBypass(); + } case ActivityType.EnabledConditionalAccessAutomations: { return TAGGED_TEMPLATES.enabledConditionalAccessAutomations(activity); } diff --git a/frontend/pages/admin/IntegrationsPage/cards/ConditionalAccess/ConditionalAccess.tsx b/frontend/pages/admin/IntegrationsPage/cards/ConditionalAccess/ConditionalAccess.tsx index b14ba3431d..3d9b90138c 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/ConditionalAccess/ConditionalAccess.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/ConditionalAccess/ConditionalAccess.tsx @@ -19,13 +19,16 @@ import { LEARN_MORE_ABOUT_BASE_LINK, } from "utilities/constants"; import Button from "components/buttons/Button"; +import Checkbox from "components/forms/fields/Checkbox"; import { AppContext } from "context/app"; import Spinner from "components/Spinner"; import PremiumFeatureMessage from "components/PremiumFeatureMessage"; import { useQuery } from "react-query"; import DataError from "components/DataError"; import Modal from "components/Modal"; +import TooltipWrapper from "components/TooltipWrapper"; import { IConfig, isOktaConditionalAccessConfigured } from "interfaces/config"; +import { IInputFieldParseTarget } from "interfaces/form_field"; import SectionCard from "../MdmSettings/components/SectionCard"; import EntraConditionalAccessModal from "./components/EntraConditionalAccessModal"; @@ -177,6 +180,12 @@ const ConditionalAccess = () => { "microsoft-entra" | "okta" | null >(null); + // Bypass disabled state + const [bypassDisabled, setBypassDisabled] = useState( + config?.conditional_access?.bypass_disabled || false + ); + const [isUpdatingBypass, setIsUpdatingBypass] = useState(false); + // "loading" state here is encompassed by phase === Phase.ConfirmingConfigured state, don't need // to use useQuery's // "error" state handled by onError callback @@ -325,6 +334,42 @@ const ConditionalAccess = () => { setProviderToDelete("okta"); }; + const onBypassDisabledChange = ({ + value, + }: IInputFieldParseTarget) => { + setBypassDisabled(value); + }; + + const handleSaveBypassSettings = async (evt: React.FormEvent) => { + evt.preventDefault(); + setIsUpdatingBypass(true); + try { + const updatedConfig = await configAPI.update({ + conditional_access: { + bypass_disabled: bypassDisabled, + // Preserve existing settings + okta_idp_id: config?.conditional_access?.okta_idp_id || "", + okta_assertion_consumer_service_url: + config?.conditional_access?.okta_assertion_consumer_service_url || + "", + okta_audience_uri: + config?.conditional_access?.okta_audience_uri || "", + okta_certificate: config?.conditional_access?.okta_certificate || "", + microsoft_entra_tenant_id: + config?.conditional_access?.microsoft_entra_tenant_id || "", + }, + }); + setConfig(updatedConfig); + renderFlash( + "success", + "Successfully updated conditional access settings." + ); + } catch { + renderFlash("error", "Could not update conditional access settings."); + } + setIsUpdatingBypass(false); + }; + // RENDER const renderOktaContent = () => { @@ -426,32 +471,78 @@ const ConditionalAccess = () => { return (
- -

- Block hosts failing policies from logging in with single sign-on. Once - connected, enable or disable on the{" "} - page. -

- {renderContent()} - {showEntraModal && ( - - )} - {showOktaModal && ( - - )} - {providerToDelete && ( - +
+ +

+ Block hosts failing policies from logging in with single sign-on. Once + connected, enable or disable on the{" "} + page. +

+ {renderContent()} + {showEntraModal && ( + + )} + {showOktaModal && ( + + )} + {providerToDelete && ( + + )} +
+ {(oktaConfigured || entraPhase === EntraPhase.Configured) && ( +
+ +
+ + + When enabled, end users will have the option to bypass Okta + conditional access if they are unable to resolve failing + policies.{" "} + + (Default: Off) + +
+
+ Bypassing is valid for a single login attempt and is tracked + in audit logs. + + } + showArrow={false} + > + Allow end user to temporarily restore access +
+
+ +
+
)}
); diff --git a/frontend/pages/admin/IntegrationsPage/cards/ConditionalAccess/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/ConditionalAccess/_styles.scss index b93be00c3f..d150389861 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/ConditionalAccess/_styles.scss +++ b/frontend/pages/admin/IntegrationsPage/cards/ConditionalAccess/_styles.scss @@ -1,4 +1,8 @@ .conditional-access { + display: flex; + flex-direction: column; + gap: 20px; + &__page-description { margin-bottom: $pad-large; } @@ -29,4 +33,10 @@ } } } + + &__end-user-experience { + .section-header { + padding-bottom: 14px; + } + } }